Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 165 additions & 3 deletions src/main/java/de/rub/nds/scanner/core/config/ExecutorConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
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;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

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;

Expand Down Expand Up @@ -60,14 +68,38 @@ public final class ExecutorConfig {
converter = ProbeTypeConverter.class)
private List<ProbeType> excludedProbes = new LinkedList<>();

@Parameter(
names = "-profile",
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. Entries in 'inheritedFromProfiles' are paths to other"
+ " profile JSON files, resolved relative to this profile's own"
+ " directory. Probes excluded via -exclude are removed regardless of"
+ " where they came from.")
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<ProbeType> probes = null;

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.
* 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
*/
Expand All @@ -84,12 +116,83 @@ public void setExcludedProbes(List<ProbeType> excludedProbes) {
this.excludedProbes = new LinkedList<>(excludedProbes);
}

/**
* Returns the path to the scan profile JSON file, if one was configured.
*
* @return the scan profile path, or null if not set
*/
public String getProfile() {
return profile;
}

/**
* Sets the path to the scan profile JSON file to use for this scan. Takes effect the next time
* {@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.profileProbeSelectorResolved = false;
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
* #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;
}
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) {
try {
SETTINGS_MAPPER.readerForUpdating(this).readValue(settings);
} catch (IOException e) {
throw new UncheckedIOException(
"Could not apply settings from scan profile '" + profile + "'", e);
}
}
Comment thread
FelixLange1998 marked this conversation as resolved.
settingsResolved = true;
}

/**
* Returns the scanner detail level for the scan operation.
*
* @return the current scanner detail level
*/
public ScannerDetail getScanDetail() {
resolveSettingsFromProfileIfNecessary();
Comment thread
FelixLange1998 marked this conversation as resolved.
return scanDetail;
}

Expand All @@ -108,6 +211,7 @@ public void setScanDetail(ScannerDetail scanDetail) {
* @return the current post-analysis detail level
*/
public ScannerDetail getPostAnalysisDetail() {
resolveSettingsFromProfileIfNecessary();
return postAnalysisDetail;
}

Expand All @@ -126,6 +230,7 @@ public void setPostAnalysisDetail(ScannerDetail postAnalysisDetail) {
* @return the current report detail level
*/
public ScannerDetail getReportDetail() {
resolveSettingsFromProfileIfNecessary();
return reportDetail;
}

Expand All @@ -144,6 +249,7 @@ public void setReportDetail(ScannerDetail reportDetail) {
* @return true if colored text is disabled, false otherwise
*/
public boolean isNoColor() {
resolveSettingsFromProfileIfNecessary();
return noColor;
}

Expand All @@ -157,7 +263,10 @@ 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, 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
*/
Expand All @@ -179,6 +288,7 @@ public void setProbes(List<ProbeType> probes) {
*
* @param probes the probe types to execute
*/
@JsonIgnore
public void setProbes(ProbeType... probes) {
this.probes = Arrays.asList(probes);
}
Expand Down Expand Up @@ -207,12 +317,60 @@ public void addProbes(ProbeType... probes) {
this.probes.addAll(Arrays.asList(probes));
}

/**
* Determines whether a candidate probe should be executed, combining every selection mechanism
* this config supports:
*
* <ol>
* <li>If {@link #setProbes(List)}/{@link #addProbes(List)} configured an explicit inclusion
* list, {@code probeType} must be contained in it.
* <li>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).
* <li>Otherwise, {@code executeByDefault} decides.
* </ol>
*
* 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.
*
* @return the probe timeout in milliseconds
*/
public int getProbeTimeout() {
resolveSettingsFromProfileIfNecessary();
return probeTimeout;
}

Expand All @@ -231,6 +389,7 @@ public void setProbeTimeout(int probeTimeout) {
* @return true if an output file is specified, false otherwise
*/
public boolean isWriteReportToFile() {
resolveSettingsFromProfileIfNecessary();
return outputFile != null;
}

Expand All @@ -240,6 +399,7 @@ public boolean isWriteReportToFile() {
* @return the output file path, or null if not specified
*/
public String getOutputFile() {
resolveSettingsFromProfileIfNecessary();
return outputFile;
}

Expand All @@ -258,6 +418,7 @@ public void setOutputFile(String outputFile) {
* @return the number of parallel probe threads
*/
public int getParallelProbes() {
resolveSettingsFromProfileIfNecessary();
return parallelProbes;
}

Expand All @@ -276,6 +437,7 @@ public void setParallelProbes(int parallelProbes) {
* @return the maximum number of overall threads
*/
public int getOverallThreads() {
resolveSettingsFromProfileIfNecessary();
return overallThreads;
}

Expand All @@ -294,6 +456,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;
}
}
58 changes: 58 additions & 0 deletions src/main/java/de/rub/nds/scanner/core/config/ProbeTypeCatalog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.ArrayList;
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<Class<? extends ProbeType>> probeTypeClasses) {
Map<String, List<String>> probesByType = new LinkedHashMap<>();
for (Class<? extends ProbeType> probeTypeClass : probeTypeClasses) {
List<String> constantNames = new ArrayList<>();
for (Object constant : probeTypeClass.getEnumConstants()) {
constantNames.add(((Enum<?>) constant).name());
}
probesByType.put(probeTypeClass.getName(), constantNames);
}
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);
}
}
}
Loading