From 1cad30f2fefb2f7572a853737f7f5d0303c914c2 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 09:32:30 +0200 Subject: [PATCH 1/9] feat: basic scanning profiles --- .../scanner/core/config/ExecutorConfig.java | 55 +++++-- .../scanner/core/config/ProbeReference.java | 97 ++++++++++++ .../nds/scanner/core/config/ScanProfile.java | 98 ++++++++++++ .../scanner/core/config/ScanProfileIO.java | 121 +++++++++++++++ .../nds/scanner/core/execution/Scanner.java | 6 +- .../core/probe/ProbeTypeConverter.java | 59 -------- .../core/config/ExecutorConfigTest.java | 75 ++++++++-- .../core/config/ScanProfileIOTest.java | 141 ++++++++++++++++++ .../core/config/SecondTestProbeType.java | 25 ++++ .../scanner/core/execution/ScannerTest.java | 10 -- 10 files changed, 589 insertions(+), 98 deletions(-) create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java delete mode 100644 src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java create mode 100644 src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java create mode 100644 src/test/java/de/rub/nds/scanner/core/config/SecondTestProbeType.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java index e3ae6643..42e5f6f3 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java @@ -10,7 +10,9 @@ import com.beust.jcommander.Parameter; import de.rub.nds.scanner.core.probe.ProbeType; -import de.rub.nds.scanner.core.probe.ProbeTypeConverter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.LinkedList; import java.util.List; @@ -54,34 +56,39 @@ public final class ExecutorConfig { private int overallThreads = 1; @Parameter( - names = "-exclude", + names = "-profile", description = - "A list of probes that should be excluded from the scan. The list is separated by commas.", - converter = ProbeTypeConverter.class) - private List excludedProbes = new LinkedList<>(); + "Path to a scan profile JSON file. Only probes declared by this profile (and" + + " any profiles it inherits from via 'inheritedFromProfiles') will be" + + " executed. Profiles it inherits from are looked up by name among the" + + " other *.json files in the same directory.") + private String profile = null; private List probes = null; + private boolean profileResolved = false; public ExecutorConfig() { // Default constructor } /** - * Returns a copy of the list of probe types that are excluded from scanning. + * Returns the path to the scan profile JSON file, if one was configured. * - * @return a new list containing the excluded probe types + * @return the scan profile path, or null if not set */ - public List getExcludedProbes() { - return new LinkedList<>(excludedProbes); + public String getProfile() { + return profile; } /** - * Sets the list of probe types to be excluded from scanning. + * Sets the path to the scan profile JSON file to use for this scan. Takes effect the next time + * {@link #getProbes()} is called. * - * @param excludedProbes the list of probe types to exclude + * @param profile the scan profile path, or null to clear */ - public void setExcludedProbes(List excludedProbes) { - this.excludedProbes = new LinkedList<>(excludedProbes); + public void setProfile(String profile) { + this.profile = profile; + this.profileResolved = false; } /** @@ -157,14 +164,30 @@ public void setNoColor(boolean noColor) { } /** - * Returns a copy of the list of probe types to be executed. + * Returns a copy of the list of probe types to be executed. If a scan profile was configured + * via {@link #setProfile(String)} (or the {@code -profile} parameter) and no probes have been + * set explicitly since, the profile is resolved (including any inherited profiles) on first + * access. * * @return a new list containing the probe types, or null if not set */ public List getProbes() { + resolveProfileIfNecessary(); return probes == null ? null : new LinkedList<>(probes); } + private void resolveProfileIfNecessary() { + if (profileResolved || profile == null) { + return; + } + try { + probes = ScanProfileIO.resolveProbes(Path.of(profile)); + } catch (IOException e) { + throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); + } + profileResolved = true; + } + /** * Sets the list of probe types to be executed. * @@ -172,6 +195,7 @@ public List getProbes() { */ public void setProbes(List probes) { this.probes = probes == null ? null : new LinkedList<>(probes); + this.profileResolved = true; } /** @@ -181,6 +205,7 @@ public void setProbes(List probes) { */ public void setProbes(ProbeType... probes) { this.probes = Arrays.asList(probes); + this.profileResolved = true; } /** @@ -189,6 +214,7 @@ public void setProbes(ProbeType... probes) { * @param probes the list of probe types to add */ public void addProbes(List probes) { + resolveProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } @@ -201,6 +227,7 @@ public void addProbes(List probes) { * @param probes the probe types to add */ public void addProbes(ProbeType... probes) { + resolveProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java new file mode 100644 index 00000000..d8da18cf --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java @@ -0,0 +1,97 @@ +/* + * 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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; + +/** + * A JSON-friendly reference to a single {@link ProbeType} constant, e.g. {@code {"type": + * "de.rub.nds.tlsattacker.core.probe.TlsProbeType", "name": "CIPHER_SUITE"}}. + * + *

{@code type} must be the fully qualified name of an enum class implementing {@link ProbeType}, + * and {@code name} must be one of its enum constant names. Representing probes this way (rather + * than relying on Jackson's polymorphic type handling on {@link ProbeType} itself) allows a single + * scan profile to freely combine probes from different {@link ProbeType} implementations, e.g. + * probes from different scanner modules. + */ +public final class ProbeReference { + + private String type; + + private String name; + + public ProbeReference() { + // Default constructor for Jackson + } + + /** + * Returns the fully qualified name of the enum class implementing {@link ProbeType}. + * + * @return the probe type's class name + */ + public String getType() { + return type; + } + + /** + * Sets the fully qualified name of the enum class implementing {@link ProbeType}. + * + * @param type the probe type's class name + */ + public void setType(String type) { + this.type = type; + } + + /** + * Returns the referenced enum constant's name. + * + * @return the probe's constant name + */ + public String getName() { + return name; + } + + /** + * Sets the referenced enum constant's name. + * + * @param name the probe's constant name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Resolves this reference to the concrete {@link ProbeType} enum constant it identifies. + * + * @return the resolved probe type + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public ProbeType resolve() { + if (type == null || name == null) { + throw new IllegalArgumentException( + "Invalid probe reference: expected an object with 'type' and 'name' fields"); + } + Class probeTypeClass; + try { + probeTypeClass = Class.forName(type); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Could not find ProbeType class '" + type + "'", e); + } + if (!ProbeType.class.isAssignableFrom(probeTypeClass) || !probeTypeClass.isEnum()) { + throw new IllegalArgumentException( + "Class '" + type + "' does not implement ProbeType as an enum"); + } + try { + return (ProbeType) Enum.valueOf((Class) probeTypeClass, name); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "'" + name + "' is not a valid constant of ProbeType enum '" + type + "'", e); + } + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java new file mode 100644 index 00000000..f9b72037 --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A named, JSON-defined scan profile. A profile declares the set of {@link ProbeType}s that should + * be executed during a scan, and may additionally inherit probes from other profiles by name via + * {@link #getInheritedFromProfiles()}. + */ +public final class ScanProfile { + + private String name; + + private List inheritedFromProfiles = new ArrayList<>(); + + private List probes = new ArrayList<>(); + + public ScanProfile() { + // Default constructor for Jackson + } + + /** + * Returns the name of this profile. + * + * @return the profile name + */ + public String getName() { + return name; + } + + /** + * Sets the name of this profile. + * + * @param name the profile name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the names of the profiles this profile inherits probes from. + * + * @return the list of inherited profile names + */ + public List getInheritedFromProfiles() { + return inheritedFromProfiles; + } + + /** + * Sets the names of the profiles this profile inherits probes from. + * + * @param inheritedFromProfiles the list of inherited profile names + */ + public void setInheritedFromProfiles(List inheritedFromProfiles) { + this.inheritedFromProfiles = + inheritedFromProfiles == null ? new ArrayList<>() : inheritedFromProfiles; + } + + /** + * Returns the raw probe references declared directly by this profile (not including inherited + * ones). + * + * @return the list of probe references declared by this profile + */ + public List getProbes() { + return probes; + } + + /** + * Sets the raw probe references declared directly by this profile. + * + * @param probes the list of probe references to declare + */ + public void setProbes(List probes) { + this.probes = probes == null ? new ArrayList<>() : probes; + } + + /** + * Resolves the probes declared directly by this profile (not including inherited ones) to their + * concrete {@link ProbeType} enum constants. + * + * @return the resolved list of probes declared by this profile + */ + public List resolveProbes() { + return probes.stream().map(ProbeReference::resolve).collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java new file mode 100644 index 00000000..681f5b50 --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java @@ -0,0 +1,121 @@ +/* + * 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.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.rub.nds.scanner.core.probe.ProbeType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Loads {@link ScanProfile}s from JSON files and resolves a profile's fully inherited set of {@link + * ProbeType}s. + */ +public final class ScanProfileIO { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private ScanProfileIO() { + // Utility class + } + + /** + * Reads a single {@link ScanProfile} from a JSON file. + * + * @param profilePath the path to the profile's JSON file + * @return the parsed profile + * @throws IOException if the file cannot be read or parsed + */ + public static ScanProfile read(Path profilePath) throws IOException { + return MAPPER.readValue(profilePath.toFile(), ScanProfile.class); + } + + /** + * Resolves the fully inherited, deduplicated list of probes for the profile stored at {@code + * profilePath}. Sibling {@code *.json} files in the same directory are parsed as candidate + * profiles to resolve entries of {@link ScanProfile#getInheritedFromProfiles()} against, + * matched by their {@code name} field. + * + * @param profilePath the path to the active profile's JSON file + * @return the deduplicated, order-preserving list of probes declared by the profile and all of + * its (transitively) inherited profiles + * @throws IOException if a profile file cannot be read or parsed + */ + public static List resolveProbes(Path profilePath) throws IOException { + ScanProfile rootProfile = read(profilePath); + Path directory = profilePath.toAbsolutePath().getParent(); + Map profilesByName = readProfileDirectory(directory); + profilesByName.put(rootProfile.getName(), rootProfile); + return resolveProbes(rootProfile.getName(), profilesByName, new LinkedHashSet<>()); + } + + private static Map readProfileDirectory(Path directory) + throws IOException { + Map profilesByName = new LinkedHashMap<>(); + if (directory == null || !Files.isDirectory(directory)) { + return profilesByName; + } + List jsonFiles; + try (Stream files = Files.list(directory)) { + jsonFiles = + files.filter(p -> p.toString().endsWith(".json")).collect(Collectors.toList()); + } + for (Path file : jsonFiles) { + ScanProfile profile; + try { + profile = read(file); + } catch (IOException e) { + throw new IOException("Could not parse scan profile file '" + file + "'", e); + } + if (profile.getName() == null) { + continue; + } + ScanProfile previous = profilesByName.putIfAbsent(profile.getName(), profile); + if (previous != null) { + throw new IOException( + "Duplicate scan profile name '" + + profile.getName() + + "' found in directory '" + + directory + + "'"); + } + } + return profilesByName; + } + + private static List resolveProbes( + String profileName, Map profilesByName, Set visiting) { + if (!visiting.add(profileName)) { + throw new IllegalStateException( + "Cyclic scan profile inheritance detected involving profile '" + + profileName + + "'"); + } + ScanProfile profile = profilesByName.get(profileName); + if (profile == null) { + throw new IllegalArgumentException("Unknown scan profile: '" + profileName + "'"); + } + LinkedHashSet resolved = new LinkedHashSet<>(); + for (String parentName : profile.getInheritedFromProfiles()) { + resolved.addAll(resolveProbes(parentName, profilesByName, visiting)); + } + resolved.addAll(profile.resolveProbes()); + visiting.remove(profileName); + return new ArrayList<>(resolved); + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java b/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java index 4ff80ff0..eb86a195 100644 --- a/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java +++ b/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java @@ -303,11 +303,7 @@ protected void registerProbeForExecution(ProbeT probe, boolean executeByDefault) if ((executorConfig.getProbes() == null && executeByDefault) || (executorConfig.getProbes() != null && executorConfig.getProbes().contains(probe.getType()))) { - if (executorConfig.getExcludedProbes().contains(probe.getType())) { - LOGGER.debug("Probe {} is excluded from the scan", probe.getType()); - } else { - probeList.add(probe); - } + probeList.add(probe); } } diff --git a/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java b/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java deleted file mode 100644 index d13e9fba..00000000 --- a/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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; - -import com.beust.jcommander.IStringConverter; -import java.lang.reflect.InvocationTargetException; -import java.util.Set; -import java.util.stream.Collectors; -import org.reflections.Reflections; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; -import org.reflections.util.FilterBuilder; - -public class ProbeTypeConverter implements IStringConverter { - - private Set> probeTypeClasses; - - public ProbeTypeConverter() { - String packageName = "de.rub"; - Reflections reflections = - new Reflections( - new ConfigurationBuilder() - .setUrls(ClasspathHelper.forPackage(packageName)) - .filterInputsBy(new FilterBuilder().includePackage(packageName))); - probeTypeClasses = - reflections.getSubTypesOf(ProbeType.class).stream() - .filter(listed -> !listed.isInterface()) - .collect(Collectors.toSet()); - } - - @Override - public ProbeType convert(String value) { - for (Class probeTypeClass : probeTypeClasses) { - // Call valueof method of each enum class - try { - ProbeType convertedType = - (ProbeType) - probeTypeClass - .getMethod("valueOf", String.class) - .invoke(null, value); - if (convertedType != null) { - return convertedType; - } - } catch (NoSuchMethodException - | IllegalAccessException - | IllegalArgumentException - | InvocationTargetException ignored) { - // Ignore conversion failures and try next method - } - } - return null; - } -} diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index 81d8f416..9bf2ac88 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -11,16 +11,23 @@ import static org.junit.jupiter.api.Assertions.*; import de.rub.nds.scanner.core.probe.ProbeType; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; public class ExecutorConfigTest { private ExecutorConfig config; + @TempDir private Path tempDir; + @BeforeEach public void setUp() { config = new ExecutorConfig(); @@ -123,19 +130,67 @@ public void testIsMultithreadedWithBoth() { } @Test - public void testExcludedProbesGetterSetter() { - assertTrue(config.getExcludedProbes().isEmpty()); + public void testProfileGetterSetter() { + assertNull(config.getProfile()); + config.setProfile("/tmp/some-profile.json"); + assertEquals("/tmp/some-profile.json", config.getProfile()); + config.setProfile(null); + assertNull(config.getProfile()); + } + + @Test + public void testGetProbesResolvesProfile() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, + "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + + de.rub.nds.scanner.core.TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + + config.setProfile(profilePath.toString()); + List probes = config.getProbes(); + + assertNotNull(probes); + assertEquals(1, probes.size()); + assertEquals(de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + } - List excludedProbes = new LinkedList<>(); - excludedProbes.add(new TestProbeType("probe1")); - excludedProbes.add(new TestProbeType("probe2")); + @Test + public void testGetProbesResolvesProfileOnlyOnce() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, + "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + + de.rub.nds.scanner.core.TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + config.setProfile(profilePath.toString()); + config.getProbes(); + + Files.delete(profilePath); + + // Should not attempt to re-read the (now deleted) file + assertEquals(1, config.getProbes().size()); + } - config.setExcludedProbes(excludedProbes); - assertEquals(2, config.getExcludedProbes().size()); + @Test + public void testSetProbesOverridesConfiguredProfile() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, + "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + + de.rub.nds.scanner.core.TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + config.setProfile(profilePath.toString()); + + config.setProbes(List.of()); + + assertTrue(config.getProbes().isEmpty()); + } - // Test that it returns a copy - config.getExcludedProbes().clear(); - assertEquals(2, config.getExcludedProbes().size()); + @Test + public void testGetProbesThrowsUncheckedIOExceptionOnMissingProfile() { + config.setProfile(tempDir.resolve("doesNotExist.json").toString()); + assertThrows(UncheckedIOException.class, () -> config.getProbes()); } @Test diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java new file mode 100644 index 00000000..fe185d19 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -0,0 +1,141 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.TestProbeType; +import de.rub.nds.scanner.core.probe.ProbeType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class ScanProfileIOTest { + + @TempDir Path tempDir; + + private void writeProfile(String fileName, String content) throws IOException { + Files.writeString(tempDir.resolve(fileName), content); + } + + @Test + public void testResolveSingleProfileWithoutInheritance() throws IOException { + writeProfile( + "solo.json", + "{" + + "\"name\": \"solo\"," + + "\"inheritedFromProfiles\": []," + + "\"probes\": [{\"type\": \"" + + TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("solo.json")); + + assertEquals(1, probes.size()); + assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + } + + @Test + public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplementations() + throws IOException { + writeProfile( + "base.json", + "{" + + "\"name\": \"base\"," + + "\"probes\": [{\"type\": \"" + + TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "}"); + writeProfile( + "combined.json", + "{" + + "\"name\": \"combined\"," + + "\"inheritedFromProfiles\": [\"base\"]," + + "\"probes\": [{\"type\": \"" + + SecondTestProbeType.class.getName() + + "\", \"name\": \"SECOND_TEST_PROBE_TYPE\"}]" + + "}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("combined.json")); + + assertEquals(2, probes.size()); + assertTrue(probes.contains(TestProbeType.TEST_PROBE_TYPE)); + assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); + } + + @Test + public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws IOException { + writeProfile( + "base.json", + "{" + + "\"name\": \"base\"," + + "\"probes\": [{\"type\": \"" + + TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "}"); + writeProfile( + "middleA.json", + "{\"name\": \"middleA\", \"inheritedFromProfiles\": [\"base\"], \"probes\": []}"); + writeProfile( + "middleB.json", + "{\"name\": \"middleB\", \"inheritedFromProfiles\": [\"base\"], \"probes\": []}"); + writeProfile( + "diamond.json", + "{\"name\": \"diamond\", \"inheritedFromProfiles\": [\"middleA\", \"middleB\"]," + + " \"probes\": []}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("diamond.json")); + + assertEquals(1, probes.size()); + assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + } + + @Test + public void testCyclicInheritanceThrows() throws IOException { + writeProfile( + "a.json", "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b\"], \"probes\": []}"); + writeProfile( + "b.json", "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a\"], \"probes\": []}"); + + assertThrows( + IllegalStateException.class, + () -> ScanProfileIO.resolveProbes(tempDir.resolve("a.json"))); + } + + @Test + public void testUnknownInheritedProfileThrows() throws IOException { + writeProfile( + "orphan.json", + "{\"name\": \"orphan\", \"inheritedFromProfiles\": [\"doesNotExist\"]," + + " \"probes\": []}"); + + assertThrows( + IllegalArgumentException.class, + () -> ScanProfileIO.resolveProbes(tempDir.resolve("orphan.json"))); + } + + @Test + public void testDuplicateProfileNameInDirectoryThrows() throws IOException { + writeProfile( + "first.json", "{\"name\": \"dup\", \"inheritedFromProfiles\": [], \"probes\": []}"); + writeProfile( + "second.json", + "{\"name\": \"dup\", \"inheritedFromProfiles\": [], \"probes\": []}"); + writeProfile( + "root.json", + "{\"name\": \"root\", \"inheritedFromProfiles\": [\"dup\"], \"probes\": []}"); + + assertThrows( + IOException.class, () -> ScanProfileIO.resolveProbes(tempDir.resolve("root.json"))); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/config/SecondTestProbeType.java b/src/test/java/de/rub/nds/scanner/core/config/SecondTestProbeType.java new file mode 100644 index 00000000..2b94118d --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/config/SecondTestProbeType.java @@ -0,0 +1,25 @@ +/* + * 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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; + +/** + * A second, distinct {@link ProbeType} implementation used to verify that a single scan profile can + * combine probes from different {@link ProbeType} implementations (e.g. probes from different + * scanner modules). + */ +public enum SecondTestProbeType implements ProbeType { + SECOND_TEST_PROBE_TYPE; + + @Override + public String getName() { + return name(); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java index b15c45ed..2bbd8cbf 100644 --- a/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java @@ -332,16 +332,6 @@ public void testRegisterProbeWithSpecificProbesConfig() { } } - @Test - public void testRegisterProbeWithExcludedProbes() { - executorConfig.setExcludedProbes(List.of(new TestProbeType("excluded"))); - try (TestScanner scanner = new TestScanner(executorConfig)) { - - TestProbe excludedProbe = new TestProbe(new TestProbeType("excluded")); - scanner.registerProbeForExecution(excludedProbe); - } - } - @Test public void testRegisterAfterProbe() { try (TestScanner scanner = new TestScanner(executorConfig)) { From 80ecb201c4532967d45ea11715bf91658279a97a Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 09:56:27 +0200 Subject: [PATCH 2/9] feat: settings for profiles --- .../scanner/core/config/ExecutorConfig.java | 79 ++++++-- .../nds/scanner/core/config/ScanProfile.java | 22 +++ .../core/config/ScanProfileSettings.java | 185 ++++++++++++++++++ .../core/config/ExecutorConfigTest.java | 88 +++++++++ 4 files changed, 362 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java index 42e5f6f3..9fe07177 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java @@ -65,7 +65,8 @@ public final class ExecutorConfig { private String profile = null; private List probes = null; - private boolean profileResolved = false; + private boolean probesResolved = false; + private boolean settingsResolved = false; public ExecutorConfig() { // Default constructor @@ -82,13 +83,58 @@ public String getProfile() { /** * Sets the path to the scan profile JSON file to use for this scan. Takes effect the next time - * {@link #getProbes()} is called. + * {@link #getProbes()} or one of the setting getters (e.g. {@link #getScanDetail()}) is called. * * @param profile the scan profile path, or null to clear */ public void setProfile(String profile) { this.profile = profile; - this.profileResolved = false; + this.probesResolved = false; + this.settingsResolved = false; + } + + /** + * Applies the settings declared directly by the active scan profile (not including any + * inherited profiles) on top of the current values, the first time this is called after {@link + * #setProfile(String)}. Fields the profile does not declare are left untouched. + */ + private void resolveSettingsFromProfileIfNecessary() { + if (settingsResolved || profile == null) { + return; + } + ScanProfileSettings settings; + try { + settings = ScanProfileIO.read(Path.of(profile)).getSettings(); + } catch (IOException e) { + throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); + } + if (settings != null) { + if (settings.getNoColor() != null) { + noColor = settings.getNoColor(); + } + if (settings.getScanDetail() != null) { + scanDetail = settings.getScanDetail(); + } + if (settings.getPostAnalysisDetail() != null) { + postAnalysisDetail = settings.getPostAnalysisDetail(); + } + if (settings.getReportDetail() != null) { + reportDetail = settings.getReportDetail(); + } + if (settings.getOutputFile() != null) { + outputFile = settings.getOutputFile(); + } + if (settings.getProbeTimeout() != null) { + probeTimeout = settings.getProbeTimeout(); + } + if (settings.getParallelProbes() != null) { + parallelProbes = settings.getParallelProbes(); + } + if (settings.getOverallThreads() != null) { + overallThreads = settings.getOverallThreads(); + } + } + settingsResolved = true; } /** @@ -97,6 +143,7 @@ public void setProfile(String profile) { * @return the current scanner detail level */ public ScannerDetail getScanDetail() { + resolveSettingsFromProfileIfNecessary(); return scanDetail; } @@ -115,6 +162,7 @@ public void setScanDetail(ScannerDetail scanDetail) { * @return the current post-analysis detail level */ public ScannerDetail getPostAnalysisDetail() { + resolveSettingsFromProfileIfNecessary(); return postAnalysisDetail; } @@ -133,6 +181,7 @@ public void setPostAnalysisDetail(ScannerDetail postAnalysisDetail) { * @return the current report detail level */ public ScannerDetail getReportDetail() { + resolveSettingsFromProfileIfNecessary(); return reportDetail; } @@ -151,6 +200,7 @@ public void setReportDetail(ScannerDetail reportDetail) { * @return true if colored text is disabled, false otherwise */ public boolean isNoColor() { + resolveSettingsFromProfileIfNecessary(); return noColor; } @@ -172,12 +222,12 @@ public void setNoColor(boolean noColor) { * @return a new list containing the probe types, or null if not set */ public List getProbes() { - resolveProfileIfNecessary(); + resolveProbesFromProfileIfNecessary(); return probes == null ? null : new LinkedList<>(probes); } - private void resolveProfileIfNecessary() { - if (profileResolved || profile == null) { + private void resolveProbesFromProfileIfNecessary() { + if (probesResolved || profile == null) { return; } try { @@ -185,7 +235,7 @@ private void resolveProfileIfNecessary() { } catch (IOException e) { throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); } - profileResolved = true; + probesResolved = true; } /** @@ -195,7 +245,7 @@ private void resolveProfileIfNecessary() { */ public void setProbes(List probes) { this.probes = probes == null ? null : new LinkedList<>(probes); - this.profileResolved = true; + this.probesResolved = true; } /** @@ -205,7 +255,7 @@ public void setProbes(List probes) { */ public void setProbes(ProbeType... probes) { this.probes = Arrays.asList(probes); - this.profileResolved = true; + this.probesResolved = true; } /** @@ -214,7 +264,7 @@ public void setProbes(ProbeType... probes) { * @param probes the list of probe types to add */ public void addProbes(List probes) { - resolveProfileIfNecessary(); + resolveProbesFromProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } @@ -227,7 +277,7 @@ public void addProbes(List probes) { * @param probes the probe types to add */ public void addProbes(ProbeType... probes) { - resolveProfileIfNecessary(); + resolveProbesFromProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } @@ -240,6 +290,7 @@ public void addProbes(ProbeType... probes) { * @return the probe timeout in milliseconds */ public int getProbeTimeout() { + resolveSettingsFromProfileIfNecessary(); return probeTimeout; } @@ -258,6 +309,7 @@ public void setProbeTimeout(int probeTimeout) { * @return true if an output file is specified, false otherwise */ public boolean isWriteReportToFile() { + resolveSettingsFromProfileIfNecessary(); return outputFile != null; } @@ -267,6 +319,7 @@ public boolean isWriteReportToFile() { * @return the output file path, or null if not specified */ public String getOutputFile() { + resolveSettingsFromProfileIfNecessary(); return outputFile; } @@ -285,6 +338,7 @@ public void setOutputFile(String outputFile) { * @return the number of parallel probe threads */ public int getParallelProbes() { + resolveSettingsFromProfileIfNecessary(); return parallelProbes; } @@ -303,6 +357,7 @@ public void setParallelProbes(int parallelProbes) { * @return the maximum number of overall threads */ public int getOverallThreads() { + resolveSettingsFromProfileIfNecessary(); return overallThreads; } @@ -321,6 +376,6 @@ public void setOverallThreads(int overallThreads) { * @return true if either parallel probes or overall threads is greater than 1 */ public boolean isMultithreaded() { - return parallelProbes > 1 || overallThreads > 1; + return getParallelProbes() > 1 || getOverallThreads() > 1; } } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index f9b72037..7a255f0f 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -26,6 +26,8 @@ public final class ScanProfile { private List probes = new ArrayList<>(); + private ScanProfileSettings settings; + public ScanProfile() { // Default constructor for Jackson } @@ -95,4 +97,24 @@ public void setProbes(List probes) { public List resolveProbes() { return probes.stream().map(ProbeReference::resolve).collect(Collectors.toList()); } + + /** + * Returns the {@link ExecutorConfig} setting overrides declared directly by this profile, or + * null if the profile declares none. Unlike {@link #getProbes()}, these are never inherited + * from {@link #getInheritedFromProfiles()}. + * + * @return the setting overrides, or null + */ + public ScanProfileSettings getSettings() { + return settings; + } + + /** + * Sets the {@link ExecutorConfig} setting overrides declared directly by this profile. + * + * @param settings the setting overrides, or null + */ + public void setSettings(ScanProfileSettings settings) { + this.settings = settings; + } } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java new file mode 100644 index 00000000..e20c8ea4 --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java @@ -0,0 +1,185 @@ +/* + * 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.config; + +/** + * Optional {@link ExecutorConfig} overrides that a {@link ScanProfile} may declare. Any field left + * {@code null} (i.e. not present in the profile's JSON) keeps {@link ExecutorConfig}'s built-in + * default instead of being overridden. + * + *

Unlike {@link ScanProfile#getProbes()}, these settings are never inherited from profiles + * listed in {@link ScanProfile#getInheritedFromProfiles()} — only the settings declared directly on + * the profile that was activated via {@code -profile} apply. + */ +public final class ScanProfileSettings { + + private Boolean noColor; + + private ScannerDetail scanDetail; + + private ScannerDetail postAnalysisDetail; + + private ScannerDetail reportDetail; + + private String outputFile; + + private Integer probeTimeout; + + private Integer parallelProbes; + + private Integer overallThreads; + + public ScanProfileSettings() { + // Default constructor for Jackson + } + + /** + * Returns the {@code noColor} override, or null if not set by the profile. + * + * @return the override, or null + */ + public Boolean getNoColor() { + return noColor; + } + + /** + * Sets the {@code noColor} override. + * + * @param noColor the override, or null to leave the default in place + */ + public void setNoColor(Boolean noColor) { + this.noColor = noColor; + } + + /** + * Returns the {@code scanDetail} override, or null if not set by the profile. + * + * @return the override, or null + */ + public ScannerDetail getScanDetail() { + return scanDetail; + } + + /** + * Sets the {@code scanDetail} override. + * + * @param scanDetail the override, or null to leave the default in place + */ + public void setScanDetail(ScannerDetail scanDetail) { + this.scanDetail = scanDetail; + } + + /** + * Returns the {@code postAnalysisDetail} override, or null if not set by the profile. + * + * @return the override, or null + */ + public ScannerDetail getPostAnalysisDetail() { + return postAnalysisDetail; + } + + /** + * Sets the {@code postAnalysisDetail} override. + * + * @param postAnalysisDetail the override, or null to leave the default in place + */ + public void setPostAnalysisDetail(ScannerDetail postAnalysisDetail) { + this.postAnalysisDetail = postAnalysisDetail; + } + + /** + * Returns the {@code reportDetail} override, or null if not set by the profile. + * + * @return the override, or null + */ + public ScannerDetail getReportDetail() { + return reportDetail; + } + + /** + * Sets the {@code reportDetail} override. + * + * @param reportDetail the override, or null to leave the default in place + */ + public void setReportDetail(ScannerDetail reportDetail) { + this.reportDetail = reportDetail; + } + + /** + * Returns the {@code outputFile} override, or null if not set by the profile. + * + * @return the override, or null + */ + public String getOutputFile() { + return outputFile; + } + + /** + * Sets the {@code outputFile} override. + * + * @param outputFile the override, or null to leave the default in place + */ + public void setOutputFile(String outputFile) { + this.outputFile = outputFile; + } + + /** + * Returns the {@code probeTimeout} override, or null if not set by the profile. + * + * @return the override, or null + */ + public Integer getProbeTimeout() { + return probeTimeout; + } + + /** + * Sets the {@code probeTimeout} override. + * + * @param probeTimeout the override, or null to leave the default in place + */ + public void setProbeTimeout(Integer probeTimeout) { + this.probeTimeout = probeTimeout; + } + + /** + * Returns the {@code parallelProbes} override, or null if not set by the profile. + * + * @return the override, or null + */ + public Integer getParallelProbes() { + return parallelProbes; + } + + /** + * Sets the {@code parallelProbes} override. + * + * @param parallelProbes the override, or null to leave the default in place + */ + public void setParallelProbes(Integer parallelProbes) { + this.parallelProbes = parallelProbes; + } + + /** + * Returns the {@code overallThreads} override, or null if not set by the profile. + * + * @return the override, or null + */ + public Integer getOverallThreads() { + return overallThreads; + } + + /** + * Sets the {@code overallThreads} override. + * + * @param overallThreads the override, or null to leave the default in place + */ + public void setOverallThreads(Integer overallThreads) { + this.overallThreads = overallThreads; + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index 9bf2ac88..346eae10 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -193,6 +193,94 @@ public void testGetProbesThrowsUncheckedIOExceptionOnMissingProfile() { assertThrows(UncheckedIOException.class, () -> config.getProbes()); } + @Test + public void testProfileSettingsOverrideDefaults() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, + "{\"name\": \"myProfile\", \"settings\": {" + + "\"noColor\": true," + + "\"scanDetail\": \"ALL\"," + + "\"postAnalysisDetail\": \"DETAILED\"," + + "\"reportDetail\": \"QUICK\"," + + "\"outputFile\": \"out.json\"," + + "\"probeTimeout\": 42," + + "\"parallelProbes\": 3," + + "\"overallThreads\": 5" + + "}}"); + + config.setProfile(profilePath.toString()); + + assertTrue(config.isNoColor()); + assertEquals(ScannerDetail.ALL, config.getScanDetail()); + assertEquals(ScannerDetail.DETAILED, config.getPostAnalysisDetail()); + assertEquals(ScannerDetail.QUICK, config.getReportDetail()); + assertEquals("out.json", config.getOutputFile()); + assertTrue(config.isWriteReportToFile()); + assertEquals(42, config.getProbeTimeout()); + assertEquals(3, config.getParallelProbes()); + assertEquals(5, config.getOverallThreads()); + assertTrue(config.isMultithreaded()); + } + + @Test + public void testProfileWithoutSettingsKeepsDefaults() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString(profilePath, "{\"name\": \"myProfile\"}"); + + config.setProfile(profilePath.toString()); + + assertFalse(config.isNoColor()); + assertEquals(ScannerDetail.NORMAL, config.getScanDetail()); + assertEquals(ScannerDetail.NORMAL, config.getPostAnalysisDetail()); + assertEquals(ScannerDetail.NORMAL, config.getReportDetail()); + assertNull(config.getOutputFile()); + assertEquals(1800000, config.getProbeTimeout()); + assertEquals(1, config.getParallelProbes()); + assertEquals(1, config.getOverallThreads()); + } + + @Test + public void testProfileWithPartialSettingsOnlyOverridesDeclaredFields() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, "{\"name\": \"myProfile\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + + config.setProfile(profilePath.toString()); + + assertEquals(ScannerDetail.ALL, config.getScanDetail()); + assertEquals(ScannerDetail.NORMAL, config.getReportDetail()); + assertEquals(1, config.getParallelProbes()); + } + + @Test + public void testProfileSettingsAreNotInheritedFromParentProfiles() throws IOException { + Files.writeString( + tempDir.resolve("base.json"), + "{\"name\": \"base\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + Files.writeString( + tempDir.resolve("child.json"), + "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base\"]}"); + + config.setProfile(tempDir.resolve("child.json").toString()); + + assertEquals(ScannerDetail.NORMAL, config.getScanDetail()); + } + + @Test + public void testExplicitSetScanDetailIsNotOverwrittenBeforeProfileIsSet() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, "{\"name\": \"myProfile\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + + config.setProfile(profilePath.toString()); + // Accessing a setting once resolves and locks in the profile's settings. + config.getScanDetail(); + config.setScanDetail(ScannerDetail.QUICK); + + assertEquals(ScannerDetail.QUICK, config.getScanDetail()); + } + @Test public void testProbesGetterSetterWithList() { assertNull(config.getProbes()); From d9405d9a13fb577c3207bd11c68611d769ad5b37 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 11:06:20 +0200 Subject: [PATCH 3/9] chore: inheritance via file paths --- .../scanner/core/config/ExecutorConfig.java | 5 +- .../nds/scanner/core/config/ScanProfile.java | 17 ++-- .../scanner/core/config/ScanProfileIO.java | 73 ++++----------- .../core/config/ExecutorConfigTest.java | 2 +- .../core/config/ScanProfileIOTest.java | 88 +++++++++++++------ 5 files changed, 94 insertions(+), 91 deletions(-) diff --git a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java index 9fe07177..3baff6bd 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java @@ -60,8 +60,9 @@ public final class ExecutorConfig { description = "Path to a scan profile JSON file. Only probes declared by this profile (and" + " any profiles it inherits from via 'inheritedFromProfiles') will be" - + " executed. Profiles it inherits from are looked up by name among the" - + " other *.json files in the same directory.") + + " executed. Entries in 'inheritedFromProfiles' are paths to other" + + " profile JSON files, resolved relative to this profile's own" + + " directory.") private String profile = null; private List probes = null; diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index 7a255f0f..524dbccc 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -15,8 +15,8 @@ /** * A named, JSON-defined scan profile. A profile declares the set of {@link ProbeType}s that should - * be executed during a scan, and may additionally inherit probes from other profiles by name via - * {@link #getInheritedFromProfiles()}. + * be executed during a scan, and may additionally inherit probes from other profiles, referenced by + * file path, via {@link #getInheritedFromProfiles()}. */ public final class ScanProfile { @@ -33,7 +33,8 @@ public ScanProfile() { } /** - * Returns the name of this profile. + * Returns the name of this profile. Purely informational (e.g. for error messages) — it plays + * no role in resolving {@link #getInheritedFromProfiles()}. * * @return the profile name */ @@ -51,18 +52,20 @@ public void setName(String name) { } /** - * Returns the names of the profiles this profile inherits probes from. + * Returns the paths of the profiles this profile inherits probes from, each resolved relative + * to the directory of the file this profile itself was loaded from (an absolute path is used + * as-is). * - * @return the list of inherited profile names + * @return the list of inherited profile paths */ public List getInheritedFromProfiles() { return inheritedFromProfiles; } /** - * Sets the names of the profiles this profile inherits probes from. + * Sets the paths of the profiles this profile inherits probes from. * - * @param inheritedFromProfiles the list of inherited profile names + * @param inheritedFromProfiles the list of inherited profile paths */ public void setInheritedFromProfiles(List inheritedFromProfiles) { this.inheritedFromProfiles = diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java index 681f5b50..5f521ca9 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java @@ -11,16 +11,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.rub.nds.scanner.core.probe.ProbeType; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Loads {@link ScanProfile}s from JSON files and resolves a profile's fully inherited set of {@link @@ -47,9 +42,9 @@ public static ScanProfile read(Path profilePath) throws IOException { /** * Resolves the fully inherited, deduplicated list of probes for the profile stored at {@code - * profilePath}. Sibling {@code *.json} files in the same directory are parsed as candidate - * profiles to resolve entries of {@link ScanProfile#getInheritedFromProfiles()} against, - * matched by their {@code name} field. + * profilePath}. Each entry of {@link ScanProfile#getInheritedFromProfiles()} is a path to + * another profile's JSON file, resolved relative to the directory of the profile declaring it + * (an absolute entry is used as-is). * * @param profilePath the path to the active profile's JSON file * @return the deduplicated, order-preserving list of probes declared by the profile and all of @@ -57,65 +52,31 @@ public static ScanProfile read(Path profilePath) throws IOException { * @throws IOException if a profile file cannot be read or parsed */ public static List resolveProbes(Path profilePath) throws IOException { - ScanProfile rootProfile = read(profilePath); - Path directory = profilePath.toAbsolutePath().getParent(); - Map profilesByName = readProfileDirectory(directory); - profilesByName.put(rootProfile.getName(), rootProfile); - return resolveProbes(rootProfile.getName(), profilesByName, new LinkedHashSet<>()); + return resolveProbes(profilePath.toAbsolutePath().normalize(), new LinkedHashSet<>()); } - private static Map readProfileDirectory(Path directory) + private static List resolveProbes(Path profilePath, Set visiting) throws IOException { - Map profilesByName = new LinkedHashMap<>(); - if (directory == null || !Files.isDirectory(directory)) { - return profilesByName; - } - List jsonFiles; - try (Stream files = Files.list(directory)) { - jsonFiles = - files.filter(p -> p.toString().endsWith(".json")).collect(Collectors.toList()); - } - for (Path file : jsonFiles) { - ScanProfile profile; - try { - profile = read(file); - } catch (IOException e) { - throw new IOException("Could not parse scan profile file '" + file + "'", e); - } - if (profile.getName() == null) { - continue; - } - ScanProfile previous = profilesByName.putIfAbsent(profile.getName(), profile); - if (previous != null) { - throw new IOException( - "Duplicate scan profile name '" - + profile.getName() - + "' found in directory '" - + directory - + "'"); - } - } - return profilesByName; - } - - private static List resolveProbes( - String profileName, Map profilesByName, Set visiting) { - if (!visiting.add(profileName)) { + if (!visiting.add(profilePath)) { throw new IllegalStateException( "Cyclic scan profile inheritance detected involving profile '" - + profileName + + profilePath + "'"); } - ScanProfile profile = profilesByName.get(profileName); - if (profile == null) { - throw new IllegalArgumentException("Unknown scan profile: '" + profileName + "'"); + ScanProfile profile; + try { + profile = read(profilePath); + } catch (IOException e) { + throw new IOException("Could not parse scan profile file '" + profilePath + "'", e); } + Path directory = profilePath.getParent(); LinkedHashSet resolved = new LinkedHashSet<>(); - for (String parentName : profile.getInheritedFromProfiles()) { - resolved.addAll(resolveProbes(parentName, profilesByName, visiting)); + for (String inheritedPath : profile.getInheritedFromProfiles()) { + Path parentPath = directory.resolve(inheritedPath).normalize(); + resolved.addAll(resolveProbes(parentPath, visiting)); } resolved.addAll(profile.resolveProbes()); - visiting.remove(profileName); + visiting.remove(profilePath); return new ArrayList<>(resolved); } } diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index 346eae10..e6b596ea 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -260,7 +260,7 @@ public void testProfileSettingsAreNotInheritedFromParentProfiles() throws IOExce "{\"name\": \"base\", \"settings\": {\"scanDetail\": \"ALL\"}}"); Files.writeString( tempDir.resolve("child.json"), - "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base\"]}"); + "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base.json\"]}"); config.setProfile(tempDir.resolve("child.json").toString()); diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java index fe185d19..87d18d32 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -24,7 +24,9 @@ public class ScanProfileIOTest { @TempDir Path tempDir; private void writeProfile(String fileName, String content) throws IOException { - Files.writeString(tempDir.resolve(fileName), content); + Path path = tempDir.resolve(fileName); + Files.createDirectories(path.getParent()); + Files.writeString(path, content); } @Test @@ -60,7 +62,7 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement "combined.json", "{" + "\"name\": \"combined\"," - + "\"inheritedFromProfiles\": [\"base\"]," + + "\"inheritedFromProfiles\": [\"base.json\"]," + "\"probes\": [{\"type\": \"" + SecondTestProbeType.class.getName() + "\", \"name\": \"SECOND_TEST_PROBE_TYPE\"}]" @@ -73,6 +75,53 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } + @Test + public void testInheritedFromProfilesResolvesRelativeToDeclaringFilesDirectory() + throws IOException { + writeProfile( + "parents/base.json", + "{" + + "\"name\": \"base\"," + + "\"probes\": [{\"type\": \"" + + TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "}"); + writeProfile( + "child.json", + "{\"name\": \"child\", \"inheritedFromProfiles\": [\"parents/base.json\"]," + + " \"probes\": []}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("child.json")); + + assertEquals(1, probes.size()); + assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + } + + @Test + public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { + writeProfile( + "base.json", + "{" + + "\"name\": \"base\"," + + "\"probes\": [{\"type\": \"" + + TestProbeType.class.getName() + + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "}"); + writeProfile( + "nested/child.json", + "{\"name\": \"child\", \"inheritedFromProfiles\": [\"" + + tempDir.resolve("base.json") + .toAbsolutePath() + .toString() + .replace("\\", "\\\\") + + "\"], \"probes\": []}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("nested/child.json")); + + assertEquals(1, probes.size()); + assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + } + @Test public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws IOException { writeProfile( @@ -85,14 +134,16 @@ public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws + "}"); writeProfile( "middleA.json", - "{\"name\": \"middleA\", \"inheritedFromProfiles\": [\"base\"], \"probes\": []}"); + "{\"name\": \"middleA\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" + + " []}"); writeProfile( "middleB.json", - "{\"name\": \"middleB\", \"inheritedFromProfiles\": [\"base\"], \"probes\": []}"); + "{\"name\": \"middleB\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" + + " []}"); writeProfile( "diamond.json", - "{\"name\": \"diamond\", \"inheritedFromProfiles\": [\"middleA\", \"middleB\"]," - + " \"probes\": []}"); + "{\"name\": \"diamond\", \"inheritedFromProfiles\": [\"middleA.json\"," + + " \"middleB.json\"], \"probes\": []}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("diamond.json")); @@ -103,9 +154,11 @@ public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws @Test public void testCyclicInheritanceThrows() throws IOException { writeProfile( - "a.json", "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b\"], \"probes\": []}"); + "a.json", + "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b.json\"], \"probes\": []}"); writeProfile( - "b.json", "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a\"], \"probes\": []}"); + "b.json", + "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a.json\"], \"probes\": []}"); assertThrows( IllegalStateException.class, @@ -116,26 +169,11 @@ public void testCyclicInheritanceThrows() throws IOException { public void testUnknownInheritedProfileThrows() throws IOException { writeProfile( "orphan.json", - "{\"name\": \"orphan\", \"inheritedFromProfiles\": [\"doesNotExist\"]," + "{\"name\": \"orphan\", \"inheritedFromProfiles\": [\"doesNotExist.json\"]," + " \"probes\": []}"); assertThrows( - IllegalArgumentException.class, + IOException.class, () -> ScanProfileIO.resolveProbes(tempDir.resolve("orphan.json"))); } - - @Test - public void testDuplicateProfileNameInDirectoryThrows() throws IOException { - writeProfile( - "first.json", "{\"name\": \"dup\", \"inheritedFromProfiles\": [], \"probes\": []}"); - writeProfile( - "second.json", - "{\"name\": \"dup\", \"inheritedFromProfiles\": [], \"probes\": []}"); - writeProfile( - "root.json", - "{\"name\": \"root\", \"inheritedFromProfiles\": [\"dup\"], \"probes\": []}"); - - assertThrows( - IOException.class, () -> ScanProfileIO.resolveProbes(tempDir.resolve("root.json"))); - } } From 655874a835a53e9db0bad2dfb687803407049ea6 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 11:19:38 +0200 Subject: [PATCH 4/9] refactor: list for each type --- .../scanner/core/config/ProbeReference.java | 97 ------------------- .../core/config/ProbeTypeResolver.java | 56 +++++++++++ .../nds/scanner/core/config/ScanProfile.java | 33 ++++--- .../core/config/ExecutorConfigTest.java | 12 +-- .../core/config/ScanProfileIOTest.java | 84 ++++++++++++---- 5 files changed, 148 insertions(+), 134 deletions(-) delete mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java deleted file mode 100644 index d8da18cf..00000000 --- a/src/main/java/de/rub/nds/scanner/core/config/ProbeReference.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.config; - -import de.rub.nds.scanner.core.probe.ProbeType; - -/** - * A JSON-friendly reference to a single {@link ProbeType} constant, e.g. {@code {"type": - * "de.rub.nds.tlsattacker.core.probe.TlsProbeType", "name": "CIPHER_SUITE"}}. - * - *

{@code type} must be the fully qualified name of an enum class implementing {@link ProbeType}, - * and {@code name} must be one of its enum constant names. Representing probes this way (rather - * than relying on Jackson's polymorphic type handling on {@link ProbeType} itself) allows a single - * scan profile to freely combine probes from different {@link ProbeType} implementations, e.g. - * probes from different scanner modules. - */ -public final class ProbeReference { - - private String type; - - private String name; - - public ProbeReference() { - // Default constructor for Jackson - } - - /** - * Returns the fully qualified name of the enum class implementing {@link ProbeType}. - * - * @return the probe type's class name - */ - public String getType() { - return type; - } - - /** - * Sets the fully qualified name of the enum class implementing {@link ProbeType}. - * - * @param type the probe type's class name - */ - public void setType(String type) { - this.type = type; - } - - /** - * Returns the referenced enum constant's name. - * - * @return the probe's constant name - */ - public String getName() { - return name; - } - - /** - * Sets the referenced enum constant's name. - * - * @param name the probe's constant name - */ - public void setName(String name) { - this.name = name; - } - - /** - * Resolves this reference to the concrete {@link ProbeType} enum constant it identifies. - * - * @return the resolved probe type - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - public ProbeType resolve() { - if (type == null || name == null) { - throw new IllegalArgumentException( - "Invalid probe reference: expected an object with 'type' and 'name' fields"); - } - Class probeTypeClass; - try { - probeTypeClass = Class.forName(type); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Could not find ProbeType class '" + type + "'", e); - } - if (!ProbeType.class.isAssignableFrom(probeTypeClass) || !probeTypeClass.isEnum()) { - throw new IllegalArgumentException( - "Class '" + type + "' does not implement ProbeType as an enum"); - } - try { - return (ProbeType) Enum.valueOf((Class) probeTypeClass, name); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "'" + name + "' is not a valid constant of ProbeType enum '" + type + "'", e); - } - } -} diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java new file mode 100644 index 00000000..18bec42d --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java @@ -0,0 +1,56 @@ +/* + * 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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; + +/** + * Resolves a {@link ProbeType} enum constant from its declaring class's fully qualified name and + * the constant's own name, as used by {@link ScanProfile#getProbes()}. + */ +final class ProbeTypeResolver { + + private ProbeTypeResolver() { + // Utility class + } + + /** + * Resolves the {@link ProbeType} enum constant named {@code constantName} declared by the enum + * class {@code className}. + * + * @param className the fully qualified name of an enum class implementing {@link ProbeType} + * @param constantName the enum constant's name + * @return the resolved probe type + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + static ProbeType resolve(String className, String constantName) { + Class probeTypeClass; + try { + probeTypeClass = Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException( + "Could not find ProbeType class '" + className + "'", e); + } + if (!ProbeType.class.isAssignableFrom(probeTypeClass) || !probeTypeClass.isEnum()) { + throw new IllegalArgumentException( + "Class '" + className + "' does not implement ProbeType as an enum"); + } + try { + return (ProbeType) Enum.valueOf((Class) probeTypeClass, constantName); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "'" + + constantName + + "' is not a valid constant of ProbeType enum '" + + className + + "'", + e); + } + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index 524dbccc..9e2a9516 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -10,8 +10,9 @@ import de.rub.nds.scanner.core.probe.ProbeType; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; /** * A named, JSON-defined scan profile. A profile declares the set of {@link ProbeType}s that should @@ -24,7 +25,7 @@ public final class ScanProfile { private List inheritedFromProfiles = new ArrayList<>(); - private List probes = new ArrayList<>(); + private Map> probes = new LinkedHashMap<>(); private ScanProfileSettings settings; @@ -73,22 +74,26 @@ public void setInheritedFromProfiles(List inheritedFromProfiles) { } /** - * Returns the raw probe references declared directly by this profile (not including inherited - * ones). + * Returns the raw probes declared directly by this profile (not including inherited ones), as a + * map from the fully qualified name of an enum class implementing {@link ProbeType} to the list + * of its constant names to run — e.g. {@code {"de.rub.nds.tlsscanner.core.constants + * .TlsProbeType": ["CIPHER_SUITE", "CERTIFICATE"]}}. This groups probes by type instead of + * repeating the type for every single probe, while still letting a profile freely combine + * probes from different {@link ProbeType} implementations. * - * @return the list of probe references declared by this profile + * @return the raw probes declared by this profile */ - public List getProbes() { + public Map> getProbes() { return probes; } /** - * Sets the raw probe references declared directly by this profile. + * Sets the raw probes declared directly by this profile. * - * @param probes the list of probe references to declare + * @param probes the probes to declare, grouped by {@link ProbeType} class name */ - public void setProbes(List probes) { - this.probes = probes == null ? new ArrayList<>() : probes; + public void setProbes(Map> probes) { + this.probes = probes == null ? new LinkedHashMap<>() : probes; } /** @@ -98,7 +103,13 @@ public void setProbes(List probes) { * @return the resolved list of probes declared by this profile */ public List resolveProbes() { - return probes.stream().map(ProbeReference::resolve).collect(Collectors.toList()); + List resolved = new ArrayList<>(); + for (Map.Entry> entry : probes.entrySet()) { + for (String constantName : entry.getValue()) { + resolved.add(ProbeTypeResolver.resolve(entry.getKey(), constantName)); + } + } + return resolved; } /** diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index e6b596ea..5b1c530c 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -143,9 +143,9 @@ public void testGetProbesResolvesProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + "{\"name\": \"myProfile\", \"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); List probes = config.getProbes(); @@ -160,9 +160,9 @@ public void testGetProbesResolvesProfileOnlyOnce() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + "{\"name\": \"myProfile\", \"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); config.getProbes(); @@ -177,9 +177,9 @@ public void testSetProbesOverridesConfiguredProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": [{\"type\": \"" + "{\"name\": \"myProfile\", \"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]}"); + + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); config.setProbes(List.of()); diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java index 87d18d32..f109ef25 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -36,9 +36,9 @@ public void testResolveSingleProfileWithoutInheritance() throws IOException { "{" + "\"name\": \"solo\"," + "\"inheritedFromProfiles\": []," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("solo.json")); @@ -54,18 +54,18 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement "base.json", "{" + "\"name\": \"base\"," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "combined.json", "{" + "\"name\": \"combined\"," + "\"inheritedFromProfiles\": [\"base.json\"]," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + SecondTestProbeType.class.getName() - + "\", \"name\": \"SECOND_TEST_PROBE_TYPE\"}]" + + "\": [\"SECOND_TEST_PROBE_TYPE\"]}" + "}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("combined.json")); @@ -75,6 +75,26 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } + @Test + public void testProbesAreGroupedByTypeWithMultipleConstantsPerType() throws IOException { + writeProfile( + "grouped.json", + "{" + + "\"name\": \"grouped\"," + + "\"probes\": {\"" + + TestProbeType.class.getName() + + "\": [\"TEST_PROBE_TYPE\"], \"" + + SecondTestProbeType.class.getName() + + "\": [\"SECOND_TEST_PROBE_TYPE\"]}" + + "}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("grouped.json")); + + assertEquals(2, probes.size()); + assertTrue(probes.contains(TestProbeType.TEST_PROBE_TYPE)); + assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); + } + @Test public void testInheritedFromProfilesResolvesRelativeToDeclaringFilesDirectory() throws IOException { @@ -82,14 +102,14 @@ public void testInheritedFromProfilesResolvesRelativeToDeclaringFilesDirectory() "parents/base.json", "{" + "\"name\": \"base\"," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "child.json", "{\"name\": \"child\", \"inheritedFromProfiles\": [\"parents/base.json\"]," - + " \"probes\": []}"); + + " \"probes\": {}}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("child.json")); @@ -103,9 +123,9 @@ public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { "base.json", "{" + "\"name\": \"base\"," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "nested/child.json", @@ -114,7 +134,7 @@ public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { .toAbsolutePath() .toString() .replace("\\", "\\\\") - + "\"], \"probes\": []}"); + + "\"], \"probes\": {}}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("nested/child.json")); @@ -128,22 +148,22 @@ public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws "base.json", "{" + "\"name\": \"base\"," - + "\"probes\": [{\"type\": \"" + + "\"probes\": {\"" + TestProbeType.class.getName() - + "\", \"name\": \"TEST_PROBE_TYPE\"}]" + + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "middleA.json", "{\"name\": \"middleA\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" - + " []}"); + + " {}}"); writeProfile( "middleB.json", "{\"name\": \"middleB\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" - + " []}"); + + " {}}"); writeProfile( "diamond.json", "{\"name\": \"diamond\", \"inheritedFromProfiles\": [\"middleA.json\"," - + " \"middleB.json\"], \"probes\": []}"); + + " \"middleB.json\"], \"probes\": {}}"); List probes = ScanProfileIO.resolveProbes(tempDir.resolve("diamond.json")); @@ -155,10 +175,10 @@ public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws public void testCyclicInheritanceThrows() throws IOException { writeProfile( "a.json", - "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b.json\"], \"probes\": []}"); + "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b.json\"], \"probes\": {}}"); writeProfile( "b.json", - "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a.json\"], \"probes\": []}"); + "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a.json\"], \"probes\": {}}"); assertThrows( IllegalStateException.class, @@ -170,10 +190,34 @@ public void testUnknownInheritedProfileThrows() throws IOException { writeProfile( "orphan.json", "{\"name\": \"orphan\", \"inheritedFromProfiles\": [\"doesNotExist.json\"]," - + " \"probes\": []}"); + + " \"probes\": {}}"); assertThrows( IOException.class, () -> ScanProfileIO.resolveProbes(tempDir.resolve("orphan.json"))); } + + @Test + public void testUnknownProbeTypeClassThrows() throws IOException { + writeProfile( + "badType.json", + "{\"name\": \"badType\", \"probes\": {\"does.not.Exist\": [\"FOO\"]}}"); + + assertThrows( + IllegalArgumentException.class, + () -> ScanProfileIO.resolveProbes(tempDir.resolve("badType.json"))); + } + + @Test + public void testUnknownProbeConstantThrows() throws IOException { + writeProfile( + "badConstant.json", + "{\"name\": \"badConstant\", \"probes\": {\"" + + TestProbeType.class.getName() + + "\": [\"DOES_NOT_EXIST\"]}}"); + + assertThrows( + IllegalArgumentException.class, + () -> ScanProfileIO.resolveProbes(tempDir.resolve("badConstant.json"))); + } } From c45b6779c04a9d6db09d3138457a51ff163cefee Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 12:31:38 +0200 Subject: [PATCH 5/9] feat: support glob-style for all and negations --- .../core/config/ProbeTypeResolver.java | 44 +++++++++++--- .../nds/scanner/core/config/ScanProfile.java | 28 ++++++++- .../config/MultiConstantTestProbeType.java | 26 ++++++++ .../core/config/ScanProfileIOTest.java | 59 +++++++++++++++++++ 4 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java index 18bec42d..2f0b510b 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java @@ -9,9 +9,11 @@ package de.rub.nds.scanner.core.config; import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.ArrayList; +import java.util.List; /** - * Resolves a {@link ProbeType} enum constant from its declaring class's fully qualified name and + * Resolves {@link ProbeType} enum constants from their declaring class's fully qualified name and * the constant's own name, as used by {@link ScanProfile#getProbes()}. */ final class ProbeTypeResolver { @@ -21,15 +23,13 @@ private ProbeTypeResolver() { } /** - * Resolves the {@link ProbeType} enum constant named {@code constantName} declared by the enum - * class {@code className}. + * Resolves the class named {@code className}, verifying that it is an enum implementing {@link + * ProbeType}. * * @param className the fully qualified name of an enum class implementing {@link ProbeType} - * @param constantName the enum constant's name - * @return the resolved probe type + * @return the resolved class */ - @SuppressWarnings({"unchecked", "rawtypes"}) - static ProbeType resolve(String className, String constantName) { + static Class resolveClass(String className) { Class probeTypeClass; try { probeTypeClass = Class.forName(className); @@ -41,6 +41,20 @@ static ProbeType resolve(String className, String constantName) { throw new IllegalArgumentException( "Class '" + className + "' does not implement ProbeType as an enum"); } + return probeTypeClass; + } + + /** + * Resolves the {@link ProbeType} enum constant named {@code constantName} declared by the enum + * class {@code className}. + * + * @param className the fully qualified name of an enum class implementing {@link ProbeType} + * @param constantName the enum constant's name + * @return the resolved probe type + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + static ProbeType resolve(String className, String constantName) { + Class probeTypeClass = resolveClass(className); try { return (ProbeType) Enum.valueOf((Class) probeTypeClass, constantName); } catch (IllegalArgumentException e) { @@ -53,4 +67,20 @@ static ProbeType resolve(String className, String constantName) { e); } } + + /** + * Returns the names of every constant declared by the enum class {@code className}, in + * declaration order. + * + * @param className the fully qualified name of an enum class implementing {@link ProbeType} + * @return the names of all of its constants + */ + static List allConstantNames(String className) { + Class probeTypeClass = resolveClass(className); + List names = new ArrayList<>(); + for (Object constant : probeTypeClass.getEnumConstants()) { + names.add(((Enum) constant).name()); + } + return names; + } } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index 9e2a9516..d1ff5a07 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -11,6 +11,7 @@ import de.rub.nds.scanner.core.probe.ProbeType; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -81,6 +82,12 @@ public void setInheritedFromProfiles(List inheritedFromProfiles) { * repeating the type for every single probe, while still letting a profile freely combine * probes from different {@link ProbeType} implementations. * + *

Each per-type list also accepts two special tokens, processed in order: {@code "*"} adds + * every constant declared by that type, and {@code "!CONSTANT_NAME"} removes a constant + * previously added (by name or by {@code "*"}) from that type's set. This lets an "everything" + * profile be written as {@code {"...TlsProbeType": ["*"]}} without enumerating every constant, + * and still exclude a few via {@code {"...TlsProbeType": ["*", "!TLS_LATENCY"]}}. + * * @return the raw probes declared by this profile */ public Map> getProbes() { @@ -98,15 +105,30 @@ public void setProbes(Map> probes) { /** * Resolves the probes declared directly by this profile (not including inherited ones) to their - * concrete {@link ProbeType} enum constants. + * concrete {@link ProbeType} enum constants, expanding {@code "*"} and {@code "!CONSTANT_NAME"} + * tokens as documented on {@link #getProbes()}. * * @return the resolved list of probes declared by this profile */ public List resolveProbes() { List resolved = new ArrayList<>(); for (Map.Entry> entry : probes.entrySet()) { - for (String constantName : entry.getValue()) { - resolved.add(ProbeTypeResolver.resolve(entry.getKey(), constantName)); + String className = entry.getKey(); + LinkedHashSet constantNames = new LinkedHashSet<>(); + for (String token : entry.getValue()) { + if ("*".equals(token)) { + constantNames.addAll(ProbeTypeResolver.allConstantNames(className)); + } else if (token.startsWith("!")) { + String excludedName = token.substring(1); + ProbeTypeResolver.resolve(className, excludedName); // validate, catch typos + constantNames.remove(excludedName); + } else { + ProbeTypeResolver.resolve(className, token); // validate, catch typos + constantNames.add(token); + } + } + for (String constantName : constantNames) { + resolved.add(ProbeTypeResolver.resolve(className, constantName)); } } return resolved; diff --git a/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java b/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java new file mode 100644 index 00000000..8ab53b5a --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java @@ -0,0 +1,26 @@ +/* + * 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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; + +/** + * A {@link ProbeType} implementation with several constants, used to test the {@code "*"} and + * {@code "!CONSTANT_NAME"} tokens supported by {@link ScanProfile#resolveProbes()}. + */ +public enum MultiConstantTestProbeType implements ProbeType { + FIRST, + SECOND, + THIRD; + + @Override + public String getName() { + return name(); + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java index f109ef25..684b0b47 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -220,4 +220,63 @@ public void testUnknownProbeConstantThrows() throws IOException { IllegalArgumentException.class, () -> ScanProfileIO.resolveProbes(tempDir.resolve("badConstant.json"))); } + + @Test + public void testWildcardExpandsToAllConstants() throws IOException { + writeProfile( + "everything.json", + "{\"name\": \"everything\", \"probes\": {\"" + + MultiConstantTestProbeType.class.getName() + + "\": [\"*\"]}}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("everything.json")); + + assertEquals(3, probes.size()); + assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); + assertTrue(probes.contains(MultiConstantTestProbeType.SECOND)); + assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); + } + + @Test + public void testNegationExcludesConstantAddedByWildcard() throws IOException { + writeProfile( + "mostly.json", + "{\"name\": \"mostly\", \"probes\": {\"" + + MultiConstantTestProbeType.class.getName() + + "\": [\"*\", \"!SECOND\"]}}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("mostly.json")); + + assertEquals(2, probes.size()); + assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); + assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); + assertFalse(probes.contains(MultiConstantTestProbeType.SECOND)); + } + + @Test + public void testNegationExcludesExplicitlyListedConstant() throws IOException { + writeProfile( + "explicit.json", + "{\"name\": \"explicit\", \"probes\": {\"" + + MultiConstantTestProbeType.class.getName() + + "\": [\"FIRST\", \"SECOND\", \"!FIRST\"]}}"); + + List probes = ScanProfileIO.resolveProbes(tempDir.resolve("explicit.json")); + + assertEquals(1, probes.size()); + assertEquals(MultiConstantTestProbeType.SECOND, probes.get(0)); + } + + @Test + public void testNegatingUnknownConstantThrows() throws IOException { + writeProfile( + "badNegation.json", + "{\"name\": \"badNegation\", \"probes\": {\"" + + MultiConstantTestProbeType.class.getName() + + "\": [\"*\", \"!DOES_NOT_EXIST\"]}}"); + + assertThrows( + IllegalArgumentException.class, + () -> ScanProfileIO.resolveProbes(tempDir.resolve("badNegation.json"))); + } } From 419087b638ce6dc853e7ef55c862486aa9c6c7a8 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 13:46:23 +0200 Subject: [PATCH 6/9] feat: list probes command --- .../scanner/core/config/ExecutorConfig.java | 27 +++++++++ .../scanner/core/config/ProbeTypeCatalog.java | 55 +++++++++++++++++++ .../core/config/ProbeTypeCatalogTest.java | 51 +++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java create mode 100644 src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java index 3baff6bd..f6596c7f 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java @@ -65,6 +65,14 @@ public final class ExecutorConfig { + " directory.") private String profile = null; + @Parameter( + names = "-listProbes", + description = + "Print every available probe, grouped by ProbeType class, in the same JSON" + + " syntax used by a scan profile's 'probes' field, then exit without" + + " scanning.") + private boolean listProbes = false; + private List probes = null; private boolean probesResolved = false; private boolean settingsResolved = false; @@ -94,6 +102,25 @@ public void setProfile(String profile) { this.settingsResolved = false; } + /** + * Returns whether {@code -listProbes} was requested, i.e. whether every available probe should + * be printed instead of running a scan. + * + * @return true if the available probes should be listed and no scan performed + */ + public boolean isListProbes() { + return listProbes; + } + + /** + * Sets whether every available probe should be printed instead of running a scan. + * + * @param listProbes true to list probes instead of scanning + */ + public void setListProbes(boolean listProbes) { + this.listProbes = listProbes; + } + /** * Applies the settings declared directly by the active scan profile (not including any * inherited profiles) on top of the current values, the first time this is called after {@link diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java new file mode 100644 index 00000000..54632ba2 --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java @@ -0,0 +1,55 @@ +/* + * 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.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Renders the constants of one or more {@link ProbeType} enum classes as the same JSON syntax used + * by a {@link ScanProfile}'s {@code probes} field, so it can be copy-pasted straight into a profile + * (e.g. behind a {@code -listProbes} CLI flag implemented by a concrete scanner, which knows which + * {@link ProbeType} classes it registers). + */ +public final class ProbeTypeCatalog { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private ProbeTypeCatalog() { + // Utility class + } + + /** + * Renders every constant of the given {@link ProbeType} enum classes as pretty-printed JSON, in + * the exact shape expected by a scan profile's {@code probes} field — a map from each class's + * fully qualified name to the list of its constant names, in declaration order. + * + * @param probeTypeClasses the {@link ProbeType} enum classes to list, e.g. {@code + * List.of(TlsProbeType.class, QuicProbeType.class)} + * @return the pretty-printed JSON, ready to paste as (or into) a profile's {@code probes} field + */ + public static String toProfileProbesJson(List> probeTypeClasses) { + Map> probesByType = new LinkedHashMap<>(); + for (Class probeTypeClass : probeTypeClasses) { + probesByType.put( + probeTypeClass.getName(), + ProbeTypeResolver.allConstantNames(probeTypeClass.getName())); + } + try { + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(probesByType); + } catch (JsonProcessingException e) { + // Unreachable: probesByType only ever contains plain strings and lists thereof. + throw new IllegalStateException("Could not render probe type catalog", e); + } + } +} diff --git a/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java new file mode 100644 index 00000000..c25d1574 --- /dev/null +++ b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java @@ -0,0 +1,51 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.rub.nds.scanner.core.TestProbeType; +import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class ProbeTypeCatalogTest { + + @Test + public void testRendersOneClassPerEntryWithAllConstants() throws Exception { + String json = + ProbeTypeCatalog.toProfileProbesJson( + List.of(TestProbeType.class, MultiConstantTestProbeType.class)); + + @SuppressWarnings("unchecked") + Map> parsed = new ObjectMapper().readValue(json, Map.class); + + assertEquals(List.of("TEST_PROBE_TYPE"), parsed.get(TestProbeType.class.getName())); + assertEquals( + List.of("FIRST", "SECOND", "THIRD"), + parsed.get(MultiConstantTestProbeType.class.getName())); + } + + @Test + public void testOutputIsDirectlyUsableAsProfileProbesField() throws Exception { + String json = + ProbeTypeCatalog.toProfileProbesJson(List.of(MultiConstantTestProbeType.class)); + String profileJson = "{\"name\": \"generated\", \"probes\": " + json + "}"; + + ScanProfile profile = new ObjectMapper().readValue(profileJson, ScanProfile.class); + List probes = profile.resolveProbes(); + + assertEquals(3, probes.size()); + assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); + assertTrue(probes.contains(MultiConstantTestProbeType.SECOND)); + assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); + } +} From e8ed5c60ffabd4059ce716929823e70ee742c283 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 14:57:24 +0200 Subject: [PATCH 7/9] refactor: requested review changes --- .../scanner/core/config/ExecutorConfig.java | 152 +++++++++----- .../scanner/core/config/ProbeTypeCatalog.java | 9 +- .../core/config/ProbeTypeResolver.java | 86 -------- .../nds/scanner/core/config/ScanProfile.java | 63 ++---- .../scanner/core/config/ScanProfileIO.java | 50 +++-- .../core/config/ScanProfileSettings.java | 185 ----------------- .../nds/scanner/core/execution/Scanner.java | 4 +- .../core/config/ExecutorConfigTest.java | 91 ++++++++- .../config/MultiConstantTestProbeType.java | 2 +- .../core/config/ProbeTypeCatalogTest.java | 22 +- .../core/config/ScanProfileIOTest.java | 188 +++++++++--------- .../scanner/core/execution/ScannerTest.java | 10 + 12 files changed, 357 insertions(+), 505 deletions(-) delete mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java delete mode 100644 src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java index f6596c7f..2110c383 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java @@ -9,7 +9,11 @@ package de.rub.nds.scanner.core.config; import com.beust.jcommander.Parameter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import de.rub.nds.scanner.core.probe.ProbeType; +import de.rub.nds.scanner.core.probe.ProbeTypeConverter; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; @@ -19,6 +23,8 @@ public final class ExecutorConfig { + private static final ObjectMapper SETTINGS_MAPPER = new ObjectMapper(); + @Parameter(names = "-noColor", description = "If you use Windows or don't want colored text.") private boolean noColor = false; @@ -55,6 +61,13 @@ public final class ExecutorConfig { "The maximum number of threads used to execute probes located in the queue.") private int overallThreads = 1; + @Parameter( + names = "-exclude", + description = + "A list of probes that should be excluded from the scan. The list is separated by commas.", + converter = ProbeTypeConverter.class) + private List excludedProbes = new LinkedList<>(); + @Parameter( names = "-profile", description = @@ -62,7 +75,8 @@ public final class ExecutorConfig { + " any profiles it inherits from via 'inheritedFromProfiles') will be" + " executed. Entries in 'inheritedFromProfiles' are paths to other" + " profile JSON files, resolved relative to this profile's own" - + " directory.") + + " directory. Probes excluded via -exclude are removed regardless of" + + " where they came from.") private String profile = null; @Parameter( @@ -74,13 +88,34 @@ public final class ExecutorConfig { private boolean listProbes = false; private List probes = null; - private boolean probesResolved = false; + + private ProbeTypeSelector profileProbeSelector = null; + private boolean profileProbeSelectorResolved = false; private boolean settingsResolved = false; public ExecutorConfig() { // Default constructor } + /** + * Returns a copy of the list of probe types that are excluded from scanning, regardless of + * whether they were selected via {@link #setProbes(List)} or via a scan profile. + * + * @return a new list containing the excluded probe types + */ + public List getExcludedProbes() { + return new LinkedList<>(excludedProbes); + } + + /** + * Sets the list of probe types to be excluded from scanning. + * + * @param excludedProbes the list of probe types to exclude + */ + public void setExcludedProbes(List excludedProbes) { + this.excludedProbes = new LinkedList<>(excludedProbes); + } + /** * Returns the path to the scan profile JSON file, if one was configured. * @@ -92,13 +127,14 @@ public String getProfile() { /** * Sets the path to the scan profile JSON file to use for this scan. Takes effect the next time - * {@link #getProbes()} or one of the setting getters (e.g. {@link #getScanDetail()}) is called. + * {@link #isProbeIncluded(ProbeType, boolean)} or one of the setting getters (e.g. {@link + * #getScanDetail()}) is called. * * @param profile the scan profile path, or null to clear */ public void setProfile(String profile) { this.profile = profile; - this.probesResolved = false; + this.profileProbeSelectorResolved = false; this.settingsResolved = false; } @@ -124,42 +160,27 @@ public void setListProbes(boolean listProbes) { /** * Applies the settings declared directly by the active scan profile (not including any * inherited profiles) on top of the current values, the first time this is called after {@link - * #setProfile(String)}. Fields the profile does not declare are left untouched. + * #setProfile(String)}. Fields the profile does not declare are left untouched. This works by + * deserializing the profile's {@code settings} JSON object directly onto this instance, so + * adding a new overridable setting only requires adding the corresponding {@code @Parameter} + * field and its getter/setter above — no separate mapping to maintain. */ private void resolveSettingsFromProfileIfNecessary() { if (settingsResolved || profile == null) { return; } - ScanProfileSettings settings; + JsonNode settings; try { settings = ScanProfileIO.read(Path.of(profile)).getSettings(); } catch (IOException e) { throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); } if (settings != null) { - if (settings.getNoColor() != null) { - noColor = settings.getNoColor(); - } - if (settings.getScanDetail() != null) { - scanDetail = settings.getScanDetail(); - } - if (settings.getPostAnalysisDetail() != null) { - postAnalysisDetail = settings.getPostAnalysisDetail(); - } - if (settings.getReportDetail() != null) { - reportDetail = settings.getReportDetail(); - } - if (settings.getOutputFile() != null) { - outputFile = settings.getOutputFile(); - } - if (settings.getProbeTimeout() != null) { - probeTimeout = settings.getProbeTimeout(); - } - if (settings.getParallelProbes() != null) { - parallelProbes = settings.getParallelProbes(); - } - if (settings.getOverallThreads() != null) { - overallThreads = settings.getOverallThreads(); + try { + SETTINGS_MAPPER.readerForUpdating(this).readValue(settings); + } catch (IOException e) { + throw new UncheckedIOException( + "Could not apply settings from scan profile '" + profile + "'", e); } } settingsResolved = true; @@ -242,30 +263,17 @@ public void setNoColor(boolean noColor) { } /** - * Returns a copy of the list of probe types to be executed. If a scan profile was configured - * via {@link #setProfile(String)} (or the {@code -profile} parameter) and no probes have been - * set explicitly since, the profile is resolved (including any inherited profiles) on first - * access. + * Returns a copy of the list of probe types to be executed, as set via {@link + * #setProbes(List)}/{@link #addProbes(List)}. This is independent of any scan profile + * configured via {@link #setProfile(String)} — see {@link #isProbeIncluded(ProbeType, boolean)} + * for the combined effect of both mechanisms. * * @return a new list containing the probe types, or null if not set */ public List getProbes() { - resolveProbesFromProfileIfNecessary(); return probes == null ? null : new LinkedList<>(probes); } - private void resolveProbesFromProfileIfNecessary() { - if (probesResolved || profile == null) { - return; - } - try { - probes = ScanProfileIO.resolveProbes(Path.of(profile)); - } catch (IOException e) { - throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); - } - probesResolved = true; - } - /** * Sets the list of probe types to be executed. * @@ -273,7 +281,6 @@ private void resolveProbesFromProfileIfNecessary() { */ public void setProbes(List probes) { this.probes = probes == null ? null : new LinkedList<>(probes); - this.probesResolved = true; } /** @@ -281,9 +288,9 @@ public void setProbes(List probes) { * * @param probes the probe types to execute */ + @JsonIgnore public void setProbes(ProbeType... probes) { this.probes = Arrays.asList(probes); - this.probesResolved = true; } /** @@ -292,7 +299,6 @@ public void setProbes(ProbeType... probes) { * @param probes the list of probe types to add */ public void addProbes(List probes) { - resolveProbesFromProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } @@ -305,13 +311,59 @@ public void addProbes(List probes) { * @param probes the probe types to add */ public void addProbes(ProbeType... probes) { - resolveProbesFromProfileIfNecessary(); if (this.probes == null) { this.probes = new LinkedList<>(); } this.probes.addAll(Arrays.asList(probes)); } + /** + * Determines whether a candidate probe should be executed, combining every selection mechanism + * this config supports: + * + *

    + *
  1. If {@link #setProbes(List)}/{@link #addProbes(List)} configured an explicit inclusion + * list, {@code probeType} must be contained in it. + *
  2. Otherwise, if a scan profile is configured via {@link #setProfile(String)}, {@code + * probeType} must be matched by it (resolved lazily, once, against the actual candidate + * probes passed here rather than by reflectively resolving class names up front). + *
  3. Otherwise, {@code executeByDefault} decides. + *
+ * + * In every case, a probe named via {@code -exclude} ({@link #getExcludedProbes()}) is always + * removed, regardless of how it was otherwise selected. + * + * @param probeType the candidate probe's type + * @param executeByDefault whether the probe should run when neither an explicit probe list nor + * a profile is configured + * @return true if the probe should be executed + */ + public boolean isProbeIncluded(ProbeType probeType, boolean executeByDefault) { + if (excludedProbes.contains(probeType)) { + return false; + } + if (probes != null) { + return probes.contains(probeType); + } + resolveProfileProbeSelectorIfNecessary(); + if (profileProbeSelector != null) { + return profileProbeSelector.matches(probeType); + } + return executeByDefault; + } + + private void resolveProfileProbeSelectorIfNecessary() { + if (profileProbeSelectorResolved || profile == null) { + return; + } + try { + profileProbeSelector = ScanProfileIO.resolveProbeSelector(Path.of(profile)); + } catch (IOException e) { + throw new UncheckedIOException("Could not load scan profile '" + profile + "'", e); + } + profileProbeSelectorResolved = true; + } + /** * Returns the timeout value for each probe execution in milliseconds. * diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java index 54632ba2..2c477792 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -41,9 +42,11 @@ private ProbeTypeCatalog() { public static String toProfileProbesJson(List> probeTypeClasses) { Map> probesByType = new LinkedHashMap<>(); for (Class probeTypeClass : probeTypeClasses) { - probesByType.put( - probeTypeClass.getName(), - ProbeTypeResolver.allConstantNames(probeTypeClass.getName())); + List constantNames = new ArrayList<>(); + for (Object constant : probeTypeClass.getEnumConstants()) { + constantNames.add(((Enum) constant).name()); + } + probesByType.put(probeTypeClass.getName(), constantNames); } try { return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(probesByType); diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java deleted file mode 100644 index 2f0b510b..00000000 --- a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeResolver.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.config; - -import de.rub.nds.scanner.core.probe.ProbeType; -import java.util.ArrayList; -import java.util.List; - -/** - * Resolves {@link ProbeType} enum constants from their declaring class's fully qualified name and - * the constant's own name, as used by {@link ScanProfile#getProbes()}. - */ -final class ProbeTypeResolver { - - private ProbeTypeResolver() { - // Utility class - } - - /** - * Resolves the class named {@code className}, verifying that it is an enum implementing {@link - * ProbeType}. - * - * @param className the fully qualified name of an enum class implementing {@link ProbeType} - * @return the resolved class - */ - static Class resolveClass(String className) { - Class probeTypeClass; - try { - probeTypeClass = Class.forName(className); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException( - "Could not find ProbeType class '" + className + "'", e); - } - if (!ProbeType.class.isAssignableFrom(probeTypeClass) || !probeTypeClass.isEnum()) { - throw new IllegalArgumentException( - "Class '" + className + "' does not implement ProbeType as an enum"); - } - return probeTypeClass; - } - - /** - * Resolves the {@link ProbeType} enum constant named {@code constantName} declared by the enum - * class {@code className}. - * - * @param className the fully qualified name of an enum class implementing {@link ProbeType} - * @param constantName the enum constant's name - * @return the resolved probe type - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - static ProbeType resolve(String className, String constantName) { - Class probeTypeClass = resolveClass(className); - try { - return (ProbeType) Enum.valueOf((Class) probeTypeClass, constantName); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "'" - + constantName - + "' is not a valid constant of ProbeType enum '" - + className - + "'", - e); - } - } - - /** - * Returns the names of every constant declared by the enum class {@code className}, in - * declaration order. - * - * @param className the fully qualified name of an enum class implementing {@link ProbeType} - * @return the names of all of its constants - */ - static List allConstantNames(String className) { - Class probeTypeClass = resolveClass(className); - List names = new ArrayList<>(); - for (Object constant : probeTypeClass.getEnumConstants()) { - names.add(((Enum) constant).name()); - } - return names; - } -} diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index d1ff5a07..1338d4d7 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -8,10 +8,10 @@ */ package de.rub.nds.scanner.core.config; +import com.fasterxml.jackson.databind.JsonNode; import de.rub.nds.scanner.core.probe.ProbeType; import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -28,7 +28,7 @@ public final class ScanProfile { private Map> probes = new LinkedHashMap<>(); - private ScanProfileSettings settings; + private JsonNode settings; public ScanProfile() { // Default constructor for Jackson @@ -82,11 +82,13 @@ public void setInheritedFromProfiles(List inheritedFromProfiles) { * repeating the type for every single probe, while still letting a profile freely combine * probes from different {@link ProbeType} implementations. * - *

Each per-type list also accepts two special tokens, processed in order: {@code "*"} adds - * every constant declared by that type, and {@code "!CONSTANT_NAME"} removes a constant - * previously added (by name or by {@code "*"}) from that type's set. This lets an "everything" + *

Each per-type list also accepts two special tokens, processed in order: {@code "*"} + * selects every probe actually registered for that type, and {@code "!CONSTANT_NAME"} deselects + * one again (by name, or previously selected via {@code "*"}). This lets an "everything" * profile be written as {@code {"...TlsProbeType": ["*"]}} without enumerating every constant, - * and still exclude a few via {@code {"...TlsProbeType": ["*", "!TLS_LATENCY"]}}. + * and still exclude a few via {@code {"...TlsProbeType": ["*", "!TLS_LATENCY"]}}. These tokens + * are matched against the actual probes a scan registers (see {@link + * ScanProfileIO#resolveProbeSelector}), not resolved reflectively from the class name up front. * * @return the raw probes declared by this profile */ @@ -104,53 +106,24 @@ public void setProbes(Map> probes) { } /** - * Resolves the probes declared directly by this profile (not including inherited ones) to their - * concrete {@link ProbeType} enum constants, expanding {@code "*"} and {@code "!CONSTANT_NAME"} - * tokens as documented on {@link #getProbes()}. + * Returns the raw {@code settings} JSON object declared directly by this profile, or null if + * the profile declares none. Unlike {@link #getProbes()}, these are never inherited from {@link + * #getInheritedFromProfiles()}. {@link ExecutorConfig} applies this by deserializing it + * directly onto itself, so its shape always matches whatever {@code @Parameter} fields {@link + * ExecutorConfig} currently declares. * - * @return the resolved list of probes declared by this profile + * @return the raw settings object, or null */ - public List resolveProbes() { - List resolved = new ArrayList<>(); - for (Map.Entry> entry : probes.entrySet()) { - String className = entry.getKey(); - LinkedHashSet constantNames = new LinkedHashSet<>(); - for (String token : entry.getValue()) { - if ("*".equals(token)) { - constantNames.addAll(ProbeTypeResolver.allConstantNames(className)); - } else if (token.startsWith("!")) { - String excludedName = token.substring(1); - ProbeTypeResolver.resolve(className, excludedName); // validate, catch typos - constantNames.remove(excludedName); - } else { - ProbeTypeResolver.resolve(className, token); // validate, catch typos - constantNames.add(token); - } - } - for (String constantName : constantNames) { - resolved.add(ProbeTypeResolver.resolve(className, constantName)); - } - } - return resolved; - } - - /** - * Returns the {@link ExecutorConfig} setting overrides declared directly by this profile, or - * null if the profile declares none. Unlike {@link #getProbes()}, these are never inherited - * from {@link #getInheritedFromProfiles()}. - * - * @return the setting overrides, or null - */ - public ScanProfileSettings getSettings() { + public JsonNode getSettings() { return settings; } /** - * Sets the {@link ExecutorConfig} setting overrides declared directly by this profile. + * Sets the raw {@code settings} JSON object declared directly by this profile. * - * @param settings the setting overrides, or null + * @param settings the raw settings object, or null */ - public void setSettings(ScanProfileSettings settings) { + public void setSettings(JsonNode settings) { this.settings = settings; } } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java index 5f521ca9..26a1cd36 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileIO.java @@ -13,13 +13,15 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; /** - * Loads {@link ScanProfile}s from JSON files and resolves a profile's fully inherited set of {@link - * ProbeType}s. + * Loads {@link ScanProfile}s from JSON files and builds a {@link ProbeTypeSelector} for a profile's + * fully inherited {@code probes} declaration. */ public final class ScanProfileIO { @@ -41,22 +43,30 @@ public static ScanProfile read(Path profilePath) throws IOException { } /** - * Resolves the fully inherited, deduplicated list of probes for the profile stored at {@code - * profilePath}. Each entry of {@link ScanProfile#getInheritedFromProfiles()} is a path to - * another profile's JSON file, resolved relative to the directory of the profile declaring it - * (an absolute entry is used as-is). + * Builds a {@link ProbeTypeSelector} for the profile stored at {@code profilePath}, merging its + * {@code probes} declaration with that of every (transitively) inherited profile. Each entry of + * {@link ScanProfile#getInheritedFromProfiles()} is a path to another profile's JSON file, + * resolved relative to the directory of the profile declaring it (an absolute entry is used + * as-is). + * + *

For a given {@link ProbeType} class, the token lists declared for it by every profile in + * the inheritance chain are concatenated in inheritance order (parents first, most-derived + * profile last) before being handed to {@link ProbeTypeSelector}, so a profile can use {@code + * "!CONSTANT_NAME"} to exclude a probe that an inherited profile selected via {@code "*"} or by + * name. * * @param profilePath the path to the active profile's JSON file - * @return the deduplicated, order-preserving list of probes declared by the profile and all of - * its (transitively) inherited profiles + * @return a selector matching every probe selected by the profile and its inherited profiles * @throws IOException if a profile file cannot be read or parsed */ - public static List resolveProbes(Path profilePath) throws IOException { - return resolveProbes(profilePath.toAbsolutePath().normalize(), new LinkedHashSet<>()); + public static ProbeTypeSelector resolveProbeSelector(Path profilePath) throws IOException { + Map> mergedTokens = + resolveProbeTokens(profilePath.toAbsolutePath().normalize(), new LinkedHashSet<>()); + return new ProbeTypeSelector(mergedTokens); } - private static List resolveProbes(Path profilePath, Set visiting) - throws IOException { + private static Map> resolveProbeTokens( + Path profilePath, Set visiting) throws IOException { if (!visiting.add(profilePath)) { throw new IllegalStateException( "Cyclic scan profile inheritance detected involving profile '" @@ -70,13 +80,21 @@ private static List resolveProbes(Path profilePath, Set visitin throw new IOException("Could not parse scan profile file '" + profilePath + "'", e); } Path directory = profilePath.getParent(); - LinkedHashSet resolved = new LinkedHashSet<>(); + Map> merged = new LinkedHashMap<>(); for (String inheritedPath : profile.getInheritedFromProfiles()) { Path parentPath = directory.resolve(inheritedPath).normalize(); - resolved.addAll(resolveProbes(parentPath, visiting)); + mergeInto(merged, resolveProbeTokens(parentPath, visiting)); } - resolved.addAll(profile.resolveProbes()); + mergeInto(merged, profile.getProbes()); visiting.remove(profilePath); - return new ArrayList<>(resolved); + return merged; + } + + private static void mergeInto( + Map> target, Map> source) { + for (Map.Entry> entry : source.entrySet()) { + target.computeIfAbsent(entry.getKey(), key -> new ArrayList<>()) + .addAll(entry.getValue()); + } } } diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java deleted file mode 100644 index e20c8ea4..00000000 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfileSettings.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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.config; - -/** - * Optional {@link ExecutorConfig} overrides that a {@link ScanProfile} may declare. Any field left - * {@code null} (i.e. not present in the profile's JSON) keeps {@link ExecutorConfig}'s built-in - * default instead of being overridden. - * - *

Unlike {@link ScanProfile#getProbes()}, these settings are never inherited from profiles - * listed in {@link ScanProfile#getInheritedFromProfiles()} — only the settings declared directly on - * the profile that was activated via {@code -profile} apply. - */ -public final class ScanProfileSettings { - - private Boolean noColor; - - private ScannerDetail scanDetail; - - private ScannerDetail postAnalysisDetail; - - private ScannerDetail reportDetail; - - private String outputFile; - - private Integer probeTimeout; - - private Integer parallelProbes; - - private Integer overallThreads; - - public ScanProfileSettings() { - // Default constructor for Jackson - } - - /** - * Returns the {@code noColor} override, or null if not set by the profile. - * - * @return the override, or null - */ - public Boolean getNoColor() { - return noColor; - } - - /** - * Sets the {@code noColor} override. - * - * @param noColor the override, or null to leave the default in place - */ - public void setNoColor(Boolean noColor) { - this.noColor = noColor; - } - - /** - * Returns the {@code scanDetail} override, or null if not set by the profile. - * - * @return the override, or null - */ - public ScannerDetail getScanDetail() { - return scanDetail; - } - - /** - * Sets the {@code scanDetail} override. - * - * @param scanDetail the override, or null to leave the default in place - */ - public void setScanDetail(ScannerDetail scanDetail) { - this.scanDetail = scanDetail; - } - - /** - * Returns the {@code postAnalysisDetail} override, or null if not set by the profile. - * - * @return the override, or null - */ - public ScannerDetail getPostAnalysisDetail() { - return postAnalysisDetail; - } - - /** - * Sets the {@code postAnalysisDetail} override. - * - * @param postAnalysisDetail the override, or null to leave the default in place - */ - public void setPostAnalysisDetail(ScannerDetail postAnalysisDetail) { - this.postAnalysisDetail = postAnalysisDetail; - } - - /** - * Returns the {@code reportDetail} override, or null if not set by the profile. - * - * @return the override, or null - */ - public ScannerDetail getReportDetail() { - return reportDetail; - } - - /** - * Sets the {@code reportDetail} override. - * - * @param reportDetail the override, or null to leave the default in place - */ - public void setReportDetail(ScannerDetail reportDetail) { - this.reportDetail = reportDetail; - } - - /** - * Returns the {@code outputFile} override, or null if not set by the profile. - * - * @return the override, or null - */ - public String getOutputFile() { - return outputFile; - } - - /** - * Sets the {@code outputFile} override. - * - * @param outputFile the override, or null to leave the default in place - */ - public void setOutputFile(String outputFile) { - this.outputFile = outputFile; - } - - /** - * Returns the {@code probeTimeout} override, or null if not set by the profile. - * - * @return the override, or null - */ - public Integer getProbeTimeout() { - return probeTimeout; - } - - /** - * Sets the {@code probeTimeout} override. - * - * @param probeTimeout the override, or null to leave the default in place - */ - public void setProbeTimeout(Integer probeTimeout) { - this.probeTimeout = probeTimeout; - } - - /** - * Returns the {@code parallelProbes} override, or null if not set by the profile. - * - * @return the override, or null - */ - public Integer getParallelProbes() { - return parallelProbes; - } - - /** - * Sets the {@code parallelProbes} override. - * - * @param parallelProbes the override, or null to leave the default in place - */ - public void setParallelProbes(Integer parallelProbes) { - this.parallelProbes = parallelProbes; - } - - /** - * Returns the {@code overallThreads} override, or null if not set by the profile. - * - * @return the override, or null - */ - public Integer getOverallThreads() { - return overallThreads; - } - - /** - * Sets the {@code overallThreads} override. - * - * @param overallThreads the override, or null to leave the default in place - */ - public void setOverallThreads(Integer overallThreads) { - this.overallThreads = overallThreads; - } -} diff --git a/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java b/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java index eb86a195..604c0209 100644 --- a/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java +++ b/src/main/java/de/rub/nds/scanner/core/execution/Scanner.java @@ -300,9 +300,7 @@ protected void registerProbeForExecution(ProbeT probe) { * @param executeByDefault Whether the probe should be executed by default. */ protected void registerProbeForExecution(ProbeT probe, boolean executeByDefault) { - if ((executorConfig.getProbes() == null && executeByDefault) - || (executorConfig.getProbes() != null - && executorConfig.getProbes().contains(probe.getType()))) { + if (executorConfig.isProbeIncluded(probe.getType(), executeByDefault)) { probeList.add(probe); } } diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index 5b1c530c..792544cb 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -139,7 +139,7 @@ public void testProfileGetterSetter() { } @Test - public void testGetProbesResolvesProfile() throws IOException { + public void testIsProbeIncludedResolvesProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, @@ -148,15 +148,15 @@ public void testGetProbesResolvesProfile() throws IOException { + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); - List probes = config.getProbes(); - assertNotNull(probes); - assertEquals(1, probes.size()); - assertEquals(de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + assertTrue( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); + assertFalse(config.isProbeIncluded(SecondTestProbeType.SECOND_TEST_PROBE_TYPE, true)); } @Test - public void testGetProbesResolvesProfileOnlyOnce() throws IOException { + public void testIsProbeIncludedResolvesProfileOnlyOnce() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, @@ -164,12 +164,14 @@ public void testGetProbesResolvesProfileOnlyOnce() throws IOException { + de.rub.nds.scanner.core.TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); - config.getProbes(); + config.isProbeIncluded(de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false); Files.delete(profilePath); // Should not attempt to re-read the (now deleted) file - assertEquals(1, config.getProbes().size()); + assertTrue( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); } @Test @@ -184,13 +186,80 @@ public void testSetProbesOverridesConfiguredProfile() throws IOException { config.setProbes(List.of()); - assertTrue(config.getProbes().isEmpty()); + assertFalse( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); } @Test - public void testGetProbesThrowsUncheckedIOExceptionOnMissingProfile() { + public void testIsProbeIncludedThrowsUncheckedIOExceptionOnMissingProfile() { config.setProfile(tempDir.resolve("doesNotExist.json").toString()); - assertThrows(UncheckedIOException.class, () -> config.getProbes()); + assertThrows( + UncheckedIOException.class, + () -> + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); + } + + @Test + public void testIsProbeIncludedDefaultsWhenNoProbesOrProfileConfigured() { + assertTrue( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, true)); + assertFalse( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); + } + + @Test + public void testExcludedProbesGetterSetter() { + assertTrue(config.getExcludedProbes().isEmpty()); + + List excludedProbes = new LinkedList<>(); + excludedProbes.add(new TestProbeType("probe1")); + excludedProbes.add(new TestProbeType("probe2")); + + config.setExcludedProbes(excludedProbes); + assertEquals(2, config.getExcludedProbes().size()); + + // Test that it returns a copy + config.getExcludedProbes().clear(); + assertEquals(2, config.getExcludedProbes().size()); + } + + @Test + public void testExcludedProbesOverrideExecuteByDefault() { + ProbeType excluded = new TestProbeType("excluded"); + config.setExcludedProbes(List.of(excluded)); + + assertFalse(config.isProbeIncluded(excluded, true)); + } + + @Test + public void testExcludedProbesOverrideExplicitProbeList() { + ProbeType excluded = new TestProbeType("excluded"); + ProbeType included = new TestProbeType("included"); + config.setProbes(excluded, included); + config.setExcludedProbes(List.of(excluded)); + + assertFalse(config.isProbeIncluded(excluded, true)); + assertTrue(config.isProbeIncluded(included, true)); + } + + @Test + public void testExcludedProbesOverrideProfile() throws IOException { + Path profilePath = tempDir.resolve("myProfile.json"); + Files.writeString( + profilePath, + "{\"name\": \"myProfile\", \"probes\": {\"" + + de.rub.nds.scanner.core.TestProbeType.class.getName() + + "\": [\"TEST_PROBE_TYPE\"]}}"); + config.setProfile(profilePath.toString()); + config.setExcludedProbes(List.of(de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE)); + + assertFalse( + config.isProbeIncluded( + de.rub.nds.scanner.core.TestProbeType.TEST_PROBE_TYPE, false)); } @Test diff --git a/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java b/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java index 8ab53b5a..920a55fc 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java +++ b/src/test/java/de/rub/nds/scanner/core/config/MultiConstantTestProbeType.java @@ -12,7 +12,7 @@ /** * A {@link ProbeType} implementation with several constants, used to test the {@code "*"} and - * {@code "!CONSTANT_NAME"} tokens supported by {@link ScanProfile#resolveProbes()}. + * {@code "!CONSTANT_NAME"} tokens supported by {@link ProbeTypeSelector}. */ public enum MultiConstantTestProbeType implements ProbeType { FIRST, diff --git a/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java index c25d1574..7a6038c8 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java @@ -12,13 +12,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.rub.nds.scanner.core.TestProbeType; -import de.rub.nds.scanner.core.probe.ProbeType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; public class ProbeTypeCatalogTest { + @TempDir Path tempDir; + @Test public void testRendersOneClassPerEntryWithAllConstants() throws Exception { String json = @@ -35,17 +40,16 @@ public void testRendersOneClassPerEntryWithAllConstants() throws Exception { } @Test - public void testOutputIsDirectlyUsableAsProfileProbesField() throws Exception { + public void testOutputIsDirectlyUsableAsProfileProbesField() throws IOException { String json = ProbeTypeCatalog.toProfileProbesJson(List.of(MultiConstantTestProbeType.class)); - String profileJson = "{\"name\": \"generated\", \"probes\": " + json + "}"; + Path profilePath = tempDir.resolve("generated.json"); + Files.writeString(profilePath, "{\"name\": \"generated\", \"probes\": " + json + "}"); - ScanProfile profile = new ObjectMapper().readValue(profileJson, ScanProfile.class); - List probes = profile.resolveProbes(); + ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(profilePath); - assertEquals(3, probes.size()); - assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); - assertTrue(probes.contains(MultiConstantTestProbeType.SECOND)); - assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); + assertTrue(selector.matches(MultiConstantTestProbeType.FIRST)); + assertTrue(selector.matches(MultiConstantTestProbeType.SECOND)); + assertTrue(selector.matches(MultiConstantTestProbeType.THIRD)); } } diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java index 684b0b47..1e50a7ff 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -11,11 +11,9 @@ import static org.junit.jupiter.api.Assertions.*; import de.rub.nds.scanner.core.TestProbeType; -import de.rub.nds.scanner.core.probe.ProbeType; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -41,10 +39,10 @@ public void testResolveSingleProfileWithoutInheritance() throws IOException { + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("solo.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("solo.json")); - assertEquals(1, probes.size()); - assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + assertTrue(selector.matches(TestProbeType.TEST_PROBE_TYPE)); } @Test @@ -68,11 +66,11 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement + "\": [\"SECOND_TEST_PROBE_TYPE\"]}" + "}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("combined.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("combined.json")); - assertEquals(2, probes.size()); - assertTrue(probes.contains(TestProbeType.TEST_PROBE_TYPE)); - assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); + assertTrue(selector.matches(TestProbeType.TEST_PROBE_TYPE)); + assertTrue(selector.matches(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } @Test @@ -88,11 +86,11 @@ public void testProbesAreGroupedByTypeWithMultipleConstantsPerType() throws IOEx + "\": [\"SECOND_TEST_PROBE_TYPE\"]}" + "}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("grouped.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("grouped.json")); - assertEquals(2, probes.size()); - assertTrue(probes.contains(TestProbeType.TEST_PROBE_TYPE)); - assertTrue(probes.contains(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); + assertTrue(selector.matches(TestProbeType.TEST_PROBE_TYPE)); + assertTrue(selector.matches(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } @Test @@ -111,10 +109,10 @@ public void testInheritedFromProfilesResolvesRelativeToDeclaringFilesDirectory() "{\"name\": \"child\", \"inheritedFromProfiles\": [\"parents/base.json\"]," + " \"probes\": {}}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("child.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("child.json")); - assertEquals(1, probes.size()); - assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + assertTrue(selector.matches(TestProbeType.TEST_PROBE_TYPE)); } @Test @@ -136,39 +134,10 @@ public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { .replace("\\", "\\\\") + "\"], \"probes\": {}}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("nested/child.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("nested/child.json")); - assertEquals(1, probes.size()); - assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); - } - - @Test - public void testResolveProfileDeduplicatesProbesSharedByMultipleParents() throws IOException { - writeProfile( - "base.json", - "{" - + "\"name\": \"base\"," - + "\"probes\": {\"" - + TestProbeType.class.getName() - + "\": [\"TEST_PROBE_TYPE\"]}" - + "}"); - writeProfile( - "middleA.json", - "{\"name\": \"middleA\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" - + " {}}"); - writeProfile( - "middleB.json", - "{\"name\": \"middleB\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" - + " {}}"); - writeProfile( - "diamond.json", - "{\"name\": \"diamond\", \"inheritedFromProfiles\": [\"middleA.json\"," - + " \"middleB.json\"], \"probes\": {}}"); - - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("diamond.json")); - - assertEquals(1, probes.size()); - assertEquals(TestProbeType.TEST_PROBE_TYPE, probes.get(0)); + assertTrue(selector.matches(TestProbeType.TEST_PROBE_TYPE)); } @Test @@ -182,7 +151,7 @@ public void testCyclicInheritanceThrows() throws IOException { assertThrows( IllegalStateException.class, - () -> ScanProfileIO.resolveProbes(tempDir.resolve("a.json"))); + () -> ScanProfileIO.resolveProbeSelector(tempDir.resolve("a.json"))); } @Test @@ -194,63 +163,39 @@ public void testUnknownInheritedProfileThrows() throws IOException { assertThrows( IOException.class, - () -> ScanProfileIO.resolveProbes(tempDir.resolve("orphan.json"))); - } - - @Test - public void testUnknownProbeTypeClassThrows() throws IOException { - writeProfile( - "badType.json", - "{\"name\": \"badType\", \"probes\": {\"does.not.Exist\": [\"FOO\"]}}"); - - assertThrows( - IllegalArgumentException.class, - () -> ScanProfileIO.resolveProbes(tempDir.resolve("badType.json"))); - } - - @Test - public void testUnknownProbeConstantThrows() throws IOException { - writeProfile( - "badConstant.json", - "{\"name\": \"badConstant\", \"probes\": {\"" - + TestProbeType.class.getName() - + "\": [\"DOES_NOT_EXIST\"]}}"); - - assertThrows( - IllegalArgumentException.class, - () -> ScanProfileIO.resolveProbes(tempDir.resolve("badConstant.json"))); + () -> ScanProfileIO.resolveProbeSelector(tempDir.resolve("orphan.json"))); } @Test - public void testWildcardExpandsToAllConstants() throws IOException { + public void testWildcardMatchesEveryConstantOfThatType() throws IOException { writeProfile( "everything.json", "{\"name\": \"everything\", \"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"*\"]}}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("everything.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("everything.json")); - assertEquals(3, probes.size()); - assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); - assertTrue(probes.contains(MultiConstantTestProbeType.SECOND)); - assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); + assertTrue(selector.matches(MultiConstantTestProbeType.FIRST)); + assertTrue(selector.matches(MultiConstantTestProbeType.SECOND)); + assertTrue(selector.matches(MultiConstantTestProbeType.THIRD)); } @Test - public void testNegationExcludesConstantAddedByWildcard() throws IOException { + public void testNegationExcludesConstantSelectedByWildcard() throws IOException { writeProfile( "mostly.json", "{\"name\": \"mostly\", \"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"*\", \"!SECOND\"]}}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("mostly.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("mostly.json")); - assertEquals(2, probes.size()); - assertTrue(probes.contains(MultiConstantTestProbeType.FIRST)); - assertTrue(probes.contains(MultiConstantTestProbeType.THIRD)); - assertFalse(probes.contains(MultiConstantTestProbeType.SECOND)); + assertTrue(selector.matches(MultiConstantTestProbeType.FIRST)); + assertFalse(selector.matches(MultiConstantTestProbeType.SECOND)); + assertTrue(selector.matches(MultiConstantTestProbeType.THIRD)); } @Test @@ -261,22 +206,73 @@ public void testNegationExcludesExplicitlyListedConstant() throws IOException { + MultiConstantTestProbeType.class.getName() + "\": [\"FIRST\", \"SECOND\", \"!FIRST\"]}}"); - List probes = ScanProfileIO.resolveProbes(tempDir.resolve("explicit.json")); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("explicit.json")); - assertEquals(1, probes.size()); - assertEquals(MultiConstantTestProbeType.SECOND, probes.get(0)); + assertFalse(selector.matches(MultiConstantTestProbeType.FIRST)); + assertTrue(selector.matches(MultiConstantTestProbeType.SECOND)); + assertFalse(selector.matches(MultiConstantTestProbeType.THIRD)); } @Test - public void testNegatingUnknownConstantThrows() throws IOException { + public void testChildProfileCanExcludeConstantSelectedByParent() throws IOException { writeProfile( - "badNegation.json", - "{\"name\": \"badNegation\", \"probes\": {\"" + "base.json", + "{\"name\": \"base\", \"probes\": {\"" + + MultiConstantTestProbeType.class.getName() + + "\": [\"*\"]}}"); + writeProfile( + "child.json", + "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" + + " {\"" + MultiConstantTestProbeType.class.getName() - + "\": [\"*\", \"!DOES_NOT_EXIST\"]}}"); + + "\": [\"!SECOND\"]}}"); - assertThrows( - IllegalArgumentException.class, - () -> ScanProfileIO.resolveProbes(tempDir.resolve("badNegation.json"))); + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("child.json")); + + assertTrue(selector.matches(MultiConstantTestProbeType.FIRST)); + assertFalse(selector.matches(MultiConstantTestProbeType.SECOND)); + assertTrue(selector.matches(MultiConstantTestProbeType.THIRD)); + } + + @Test + public void testUnknownClassNameNeverMatchesInsteadOfThrowing() throws IOException { + writeProfile( + "badType.json", + "{\"name\": \"badType\", \"probes\": {\"does.not.Exist\": [\"FOO\"]}}"); + + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("badType.json")); + + assertFalse(selector.matches(TestProbeType.TEST_PROBE_TYPE)); + } + + @Test + public void testTypoedConstantNameNeverMatchesInsteadOfThrowing() throws IOException { + writeProfile( + "badConstant.json", + "{\"name\": \"badConstant\", \"probes\": {\"" + + TestProbeType.class.getName() + + "\": [\"DOES_NOT_EXIST\"]}}"); + + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("badConstant.json")); + + assertFalse(selector.matches(TestProbeType.TEST_PROBE_TYPE)); + } + + @Test + public void testUnmentionedTypeNeverMatches() throws IOException { + writeProfile( + "onlyFirst.json", + "{\"name\": \"onlyFirst\", \"probes\": {\"" + + TestProbeType.class.getName() + + "\": [\"*\"]}}"); + + ProbeTypeSelector selector = + ScanProfileIO.resolveProbeSelector(tempDir.resolve("onlyFirst.json")); + + assertFalse(selector.matches(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } } diff --git a/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java index 2bbd8cbf..b15c45ed 100644 --- a/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java +++ b/src/test/java/de/rub/nds/scanner/core/execution/ScannerTest.java @@ -332,6 +332,16 @@ public void testRegisterProbeWithSpecificProbesConfig() { } } + @Test + public void testRegisterProbeWithExcludedProbes() { + executorConfig.setExcludedProbes(List.of(new TestProbeType("excluded"))); + try (TestScanner scanner = new TestScanner(executorConfig)) { + + TestProbe excludedProbe = new TestProbe(new TestProbeType("excluded")); + scanner.registerProbeForExecution(excludedProbe); + } + } + @Test public void testRegisterAfterProbe() { try (TestScanner scanner = new TestScanner(executorConfig)) { From 2c226760a71efea1336d4670cedbb10f95ea11f7 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Thu, 3 Sep 2026 14:57:30 +0200 Subject: [PATCH 8/9] refactor: requested review changes --- .../core/config/ProbeTypeSelector.java | 65 +++++++++++++++++++ .../core/probe/ProbeTypeConverter.java | 59 +++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 src/main/java/de/rub/nds/scanner/core/config/ProbeTypeSelector.java create mode 100644 src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java diff --git a/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeSelector.java b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeSelector.java new file mode 100644 index 00000000..bd2e324b --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/config/ProbeTypeSelector.java @@ -0,0 +1,65 @@ +/* + * 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.config; + +import de.rub.nds.scanner.core.probe.ProbeType; +import java.util.List; +import java.util.Map; + +/** + * Decides whether a candidate {@link ProbeType} is selected by a scan profile's {@code probes} + * declaration (merged across its inheritance chain), by comparing the profile's raw {@code + * "className": ["TOKEN", ...]} entries directly against the candidate's actual declaring class and + * constant name. + * + *

This never resolves a class or constant by name via reflection: a candidate is only ever + * matched against classes and constants that some already-instantiated {@link ProbeType} actually + * belongs to, so a typo or a class that no longer exists in a profile simply never matches instead + * of failing to load. + */ +final class ProbeTypeSelector { + + private final Map> tokensByClassName; + + ProbeTypeSelector(Map> tokensByClassName) { + this.tokensByClassName = tokensByClassName; + } + + /** + * Determines whether {@code probeType} is selected, by replaying the tokens declared for its + * declaring class in order: {@code "*"} selects it, a matching constant name selects it, and a + * matching {@code "!CONSTANT_NAME"} deselects it again — whichever applies last wins. + * + * @param probeType the candidate probe type + * @return true if the tokens declared for this probe's class select it + */ + boolean matches(ProbeType probeType) { + if (!(probeType instanceof Enum)) { + return false; + } + Enum constant = (Enum) probeType; + List tokens = tokensByClassName.get(constant.getDeclaringClass().getName()); + if (tokens == null) { + return false; + } + boolean selected = false; + for (String token : tokens) { + if ("*".equals(token)) { + selected = true; + } else if (token.startsWith("!")) { + if (token.substring(1).equals(constant.name())) { + selected = false; + } + } else if (token.equals(constant.name())) { + selected = true; + } + } + return selected; + } +} diff --git a/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java b/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java new file mode 100644 index 00000000..d13e9fba --- /dev/null +++ b/src/main/java/de/rub/nds/scanner/core/probe/ProbeTypeConverter.java @@ -0,0 +1,59 @@ +/* + * 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; + +import com.beust.jcommander.IStringConverter; +import java.lang.reflect.InvocationTargetException; +import java.util.Set; +import java.util.stream.Collectors; +import org.reflections.Reflections; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; + +public class ProbeTypeConverter implements IStringConverter { + + private Set> probeTypeClasses; + + public ProbeTypeConverter() { + String packageName = "de.rub"; + Reflections reflections = + new Reflections( + new ConfigurationBuilder() + .setUrls(ClasspathHelper.forPackage(packageName)) + .filterInputsBy(new FilterBuilder().includePackage(packageName))); + probeTypeClasses = + reflections.getSubTypesOf(ProbeType.class).stream() + .filter(listed -> !listed.isInterface()) + .collect(Collectors.toSet()); + } + + @Override + public ProbeType convert(String value) { + for (Class probeTypeClass : probeTypeClasses) { + // Call valueof method of each enum class + try { + ProbeType convertedType = + (ProbeType) + probeTypeClass + .getMethod("valueOf", String.class) + .invoke(null, value); + if (convertedType != null) { + return convertedType; + } + } catch (NoSuchMethodException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException ignored) { + // Ignore conversion failures and try next method + } + } + return null; + } +} From 3b85ed55cc407b0819a8942cc653edd582baf1a3 Mon Sep 17 00:00:00 2001 From: FelixLange1998 Date: Fri, 4 Sep 2026 10:02:51 +0200 Subject: [PATCH 9/9] refactor: remove names --- .../nds/scanner/core/config/ScanProfile.java | 25 +------- .../core/config/ExecutorConfigTest.java | 24 +++----- .../core/config/ProbeTypeCatalogTest.java | 2 +- .../core/config/ScanProfileIOTest.java | 61 ++++++++----------- 4 files changed, 39 insertions(+), 73 deletions(-) diff --git a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java index 1338d4d7..98b3676b 100644 --- a/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java +++ b/src/main/java/de/rub/nds/scanner/core/config/ScanProfile.java @@ -16,14 +16,12 @@ import java.util.Map; /** - * A named, JSON-defined scan profile. A profile declares the set of {@link ProbeType}s that should - * be executed during a scan, and may additionally inherit probes from other profiles, referenced by + * A JSON-defined scan profile. A profile declares the set of {@link ProbeType}s that should be + * executed during a scan, and may additionally inherit probes from other profiles, referenced by * file path, via {@link #getInheritedFromProfiles()}. */ public final class ScanProfile { - private String name; - private List inheritedFromProfiles = new ArrayList<>(); private Map> probes = new LinkedHashMap<>(); @@ -34,25 +32,6 @@ public ScanProfile() { // Default constructor for Jackson } - /** - * Returns the name of this profile. Purely informational (e.g. for error messages) — it plays - * no role in resolving {@link #getInheritedFromProfiles()}. - * - * @return the profile name - */ - public String getName() { - return name; - } - - /** - * Sets the name of this profile. - * - * @param name the profile name - */ - public void setName(String name) { - this.name = name; - } - /** * Returns the paths of the profiles this profile inherits probes from, each resolved relative * to the directory of the file this profile itself was loaded from (an absolute path is used diff --git a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java index 792544cb..e671dbaf 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ExecutorConfigTest.java @@ -143,7 +143,7 @@ public void testIsProbeIncludedResolvesProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": {\"" + "{\"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}}"); @@ -160,7 +160,7 @@ public void testIsProbeIncludedResolvesProfileOnlyOnce() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": {\"" + "{\"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); @@ -179,7 +179,7 @@ public void testSetProbesOverridesConfiguredProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": {\"" + "{\"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); @@ -251,7 +251,7 @@ public void testExcludedProbesOverrideProfile() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"probes\": {\"" + "{\"probes\": {\"" + de.rub.nds.scanner.core.TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}}"); config.setProfile(profilePath.toString()); @@ -267,7 +267,7 @@ public void testProfileSettingsOverrideDefaults() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); Files.writeString( profilePath, - "{\"name\": \"myProfile\", \"settings\": {" + "{\"settings\": {" + "\"noColor\": true," + "\"scanDetail\": \"ALL\"," + "\"postAnalysisDetail\": \"DETAILED\"," @@ -295,7 +295,7 @@ public void testProfileSettingsOverrideDefaults() throws IOException { @Test public void testProfileWithoutSettingsKeepsDefaults() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); - Files.writeString(profilePath, "{\"name\": \"myProfile\"}"); + Files.writeString(profilePath, "{}"); config.setProfile(profilePath.toString()); @@ -312,8 +312,7 @@ public void testProfileWithoutSettingsKeepsDefaults() throws IOException { @Test public void testProfileWithPartialSettingsOnlyOverridesDeclaredFields() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); - Files.writeString( - profilePath, "{\"name\": \"myProfile\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + Files.writeString(profilePath, "{\"settings\": {\"scanDetail\": \"ALL\"}}"); config.setProfile(profilePath.toString()); @@ -325,11 +324,9 @@ public void testProfileWithPartialSettingsOnlyOverridesDeclaredFields() throws I @Test public void testProfileSettingsAreNotInheritedFromParentProfiles() throws IOException { Files.writeString( - tempDir.resolve("base.json"), - "{\"name\": \"base\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + tempDir.resolve("base.json"), "{\"settings\": {\"scanDetail\": \"ALL\"}}"); Files.writeString( - tempDir.resolve("child.json"), - "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base.json\"]}"); + tempDir.resolve("child.json"), "{\"inheritedFromProfiles\": [\"base.json\"]}"); config.setProfile(tempDir.resolve("child.json").toString()); @@ -339,8 +336,7 @@ public void testProfileSettingsAreNotInheritedFromParentProfiles() throws IOExce @Test public void testExplicitSetScanDetailIsNotOverwrittenBeforeProfileIsSet() throws IOException { Path profilePath = tempDir.resolve("myProfile.json"); - Files.writeString( - profilePath, "{\"name\": \"myProfile\", \"settings\": {\"scanDetail\": \"ALL\"}}"); + Files.writeString(profilePath, "{\"settings\": {\"scanDetail\": \"ALL\"}}"); config.setProfile(profilePath.toString()); // Accessing a setting once resolves and locks in the profile's settings. diff --git a/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java index 7a6038c8..d3cd915d 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ProbeTypeCatalogTest.java @@ -44,7 +44,7 @@ public void testOutputIsDirectlyUsableAsProfileProbesField() throws IOException String json = ProbeTypeCatalog.toProfileProbesJson(List.of(MultiConstantTestProbeType.class)); Path profilePath = tempDir.resolve("generated.json"); - Files.writeString(profilePath, "{\"name\": \"generated\", \"probes\": " + json + "}"); + Files.writeString(profilePath, "{\"probes\": " + json + "}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(profilePath); diff --git a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java index 1e50a7ff..b6a20243 100644 --- a/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java +++ b/src/test/java/de/rub/nds/scanner/core/config/ScanProfileIOTest.java @@ -32,7 +32,6 @@ public void testResolveSingleProfileWithoutInheritance() throws IOException { writeProfile( "solo.json", "{" - + "\"name\": \"solo\"," + "\"inheritedFromProfiles\": []," + "\"probes\": {\"" + TestProbeType.class.getName() @@ -51,7 +50,6 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement writeProfile( "base.json", "{" - + "\"name\": \"base\"," + "\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}" @@ -59,7 +57,6 @@ public void testResolveProfileWithInheritanceCombinesDifferentProbeTypeImplement writeProfile( "combined.json", "{" - + "\"name\": \"combined\"," + "\"inheritedFromProfiles\": [\"base.json\"]," + "\"probes\": {\"" + SecondTestProbeType.class.getName() @@ -78,7 +75,6 @@ public void testProbesAreGroupedByTypeWithMultipleConstantsPerType() throws IOEx writeProfile( "grouped.json", "{" - + "\"name\": \"grouped\"," + "\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"], \"" @@ -99,15 +95,13 @@ public void testInheritedFromProfilesResolvesRelativeToDeclaringFilesDirectory() writeProfile( "parents/base.json", "{" - + "\"name\": \"base\"," + "\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "child.json", - "{\"name\": \"child\", \"inheritedFromProfiles\": [\"parents/base.json\"]," - + " \"probes\": {}}"); + "{\"inheritedFromProfiles\": [\"parents/base.json\"]," + " \"probes\": {}}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(tempDir.resolve("child.json")); @@ -120,14 +114,13 @@ public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { writeProfile( "base.json", "{" - + "\"name\": \"base\"," + "\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"TEST_PROBE_TYPE\"]}" + "}"); writeProfile( "nested/child.json", - "{\"name\": \"child\", \"inheritedFromProfiles\": [\"" + "{\"inheritedFromProfiles\": [\"" + tempDir.resolve("base.json") .toAbsolutePath() .toString() @@ -142,12 +135,8 @@ public void testInheritedFromProfilesAcceptsAbsolutePath() throws IOException { @Test public void testCyclicInheritanceThrows() throws IOException { - writeProfile( - "a.json", - "{\"name\": \"a\", \"inheritedFromProfiles\": [\"b.json\"], \"probes\": {}}"); - writeProfile( - "b.json", - "{\"name\": \"b\", \"inheritedFromProfiles\": [\"a.json\"], \"probes\": {}}"); + writeProfile("a.json", "{\"inheritedFromProfiles\": [\"b.json\"], \"probes\": {}}"); + writeProfile("b.json", "{\"inheritedFromProfiles\": [\"a.json\"], \"probes\": {}}"); assertThrows( IllegalStateException.class, @@ -158,8 +147,7 @@ public void testCyclicInheritanceThrows() throws IOException { public void testUnknownInheritedProfileThrows() throws IOException { writeProfile( "orphan.json", - "{\"name\": \"orphan\", \"inheritedFromProfiles\": [\"doesNotExist.json\"]," - + " \"probes\": {}}"); + "{\"inheritedFromProfiles\": [\"doesNotExist.json\"]," + " \"probes\": {}}"); assertThrows( IOException.class, @@ -170,9 +158,7 @@ public void testUnknownInheritedProfileThrows() throws IOException { public void testWildcardMatchesEveryConstantOfThatType() throws IOException { writeProfile( "everything.json", - "{\"name\": \"everything\", \"probes\": {\"" - + MultiConstantTestProbeType.class.getName() - + "\": [\"*\"]}}"); + "{\"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"*\"]}}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(tempDir.resolve("everything.json")); @@ -186,7 +172,7 @@ public void testWildcardMatchesEveryConstantOfThatType() throws IOException { public void testNegationExcludesConstantSelectedByWildcard() throws IOException { writeProfile( "mostly.json", - "{\"name\": \"mostly\", \"probes\": {\"" + "{\"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"*\", \"!SECOND\"]}}"); @@ -202,7 +188,7 @@ public void testNegationExcludesConstantSelectedByWildcard() throws IOException public void testNegationExcludesExplicitlyListedConstant() throws IOException { writeProfile( "explicit.json", - "{\"name\": \"explicit\", \"probes\": {\"" + "{\"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"FIRST\", \"SECOND\", \"!FIRST\"]}}"); @@ -218,12 +204,10 @@ public void testNegationExcludesExplicitlyListedConstant() throws IOException { public void testChildProfileCanExcludeConstantSelectedByParent() throws IOException { writeProfile( "base.json", - "{\"name\": \"base\", \"probes\": {\"" - + MultiConstantTestProbeType.class.getName() - + "\": [\"*\"]}}"); + "{\"probes\": {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"*\"]}}"); writeProfile( "child.json", - "{\"name\": \"child\", \"inheritedFromProfiles\": [\"base.json\"], \"probes\":" + "{\"inheritedFromProfiles\": [\"base.json\"], \"probes\":" + " {\"" + MultiConstantTestProbeType.class.getName() + "\": [\"!SECOND\"]}}"); @@ -238,9 +222,7 @@ public void testChildProfileCanExcludeConstantSelectedByParent() throws IOExcept @Test public void testUnknownClassNameNeverMatchesInsteadOfThrowing() throws IOException { - writeProfile( - "badType.json", - "{\"name\": \"badType\", \"probes\": {\"does.not.Exist\": [\"FOO\"]}}"); + writeProfile("badType.json", "{\"probes\": {\"does.not.Exist\": [\"FOO\"]}}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(tempDir.resolve("badType.json")); @@ -252,9 +234,7 @@ public void testUnknownClassNameNeverMatchesInsteadOfThrowing() throws IOExcepti public void testTypoedConstantNameNeverMatchesInsteadOfThrowing() throws IOException { writeProfile( "badConstant.json", - "{\"name\": \"badConstant\", \"probes\": {\"" - + TestProbeType.class.getName() - + "\": [\"DOES_NOT_EXIST\"]}}"); + "{\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"DOES_NOT_EXIST\"]}}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(tempDir.resolve("badConstant.json")); @@ -266,13 +246,24 @@ public void testTypoedConstantNameNeverMatchesInsteadOfThrowing() throws IOExcep public void testUnmentionedTypeNeverMatches() throws IOException { writeProfile( "onlyFirst.json", - "{\"name\": \"onlyFirst\", \"probes\": {\"" - + TestProbeType.class.getName() - + "\": [\"*\"]}}"); + "{\"probes\": {\"" + TestProbeType.class.getName() + "\": [\"*\"]}}"); ProbeTypeSelector selector = ScanProfileIO.resolveProbeSelector(tempDir.resolve("onlyFirst.json")); assertFalse(selector.matches(SecondTestProbeType.SECOND_TEST_PROBE_TYPE)); } + + @Test + public void testUnknownPropertyThrows() throws IOException { + writeProfile( + "withName.json", + "{\"name\": \"legacy\", \"probes\": {\"" + + TestProbeType.class.getName() + + "\": [\"TEST_PROBE_TYPE\"]}}"); + + assertThrows( + IOException.class, + () -> ScanProfileIO.resolveProbeSelector(tempDir.resolve("withName.json"))); + } }