Skip to content

Commit ee16524

Browse files
committed
feat: Add built in playbook profiles + ability to support custom profiles
1 parent e3604ec commit ee16524

18 files changed

Lines changed: 2869 additions & 127 deletions

src/main/java/dev/dochia/cli/core/args/FilterArguments.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.dochia.cli.core.args;
22

3+
import dev.dochia.cli.core.args.util.ProfileLoader;
34
import dev.dochia.cli.core.http.HttpMethod;
45
import dev.dochia.cli.core.playbook.api.BodyPlaybook;
56
import dev.dochia.cli.core.playbook.api.EmojiPlaybook;
@@ -25,6 +26,8 @@
2526
import picocli.CommandLine;
2627

2728
import java.lang.annotation.Annotation;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
2831
import java.util.ArrayList;
2932
import java.util.Collections;
3033
import java.util.Comparator;
@@ -63,6 +66,8 @@ public class FilterArguments {
6366
@Inject
6467
@Getter
6568
ProcessingArguments processingArguments;
69+
@Inject
70+
ProfileLoader profileLoader;
6671

6772
enum FieldType {
6873
STRING, NUMBER, INTEGER, BOOLEAN
@@ -159,6 +164,16 @@ enum FormatType {
159164
"will skip BypassAuthentication playbook for all endpoints that have x-public-endpoint extension set to true.", split = ",")
160165
private List<String> skipPlaybooksForExtension;
161166

167+
@CommandLine.Option(
168+
names = {"--profile"},
169+
description = "Use a predefined playbook profile: security, quick, compliance, ci, full (default: full)")
170+
private String profile = "full";
171+
172+
@CommandLine.Option(
173+
names = {"--profile-file"},
174+
description = "Path to custom profile configuration file (YAML)")
175+
private Path customProfileFile;
176+
162177

163178
private Map<String, List<String>> skipPathPlaybooks = new HashMap<>();
164179
private Map<String, Map<String, List<String>>> skipPlaybooksForExtensionMap = new HashMap<>();
@@ -561,6 +576,47 @@ private List<String> removeSkippedPlaybooksGlobally(List<String> allowedPlaybook
561576
.toList();
562577
}
563578

579+
/**
580+
* Applies the selected playbook profile to filter which playbooks will run.
581+
* If a custom profile file is provided, it will be loaded first.
582+
* If the profile specifies an empty playbook list, all playbooks will run (full profile).
583+
* If user also specified --playbooks, the intersection of profile and user playbooks will be used.
584+
*
585+
* @param spec the PicoCli command spec for error reporting
586+
*/
587+
public void applyProfile(CommandLine.Model.CommandSpec spec) {
588+
if (customProfileFile != null && Files.exists(customProfileFile)) {
589+
profileLoader.loadCustomProfiles(customProfileFile);
590+
}
591+
592+
Optional<ProfileLoader.Profile> selectedProfile = profileLoader.getProfile(profile);
593+
594+
if (selectedProfile.isEmpty()) {
595+
throw new CommandLine.ParameterException(spec.commandLine(),
596+
"Profile '" + profile + "' not found. Available profiles: " + profileLoader.getAvailableProfiles());
597+
}
598+
599+
ProfileLoader.Profile profileConfig = selectedProfile.get();
600+
601+
if (profileConfig.playbooks().isEmpty()) {
602+
logger.info("Using profile '{}' - ALL playbooks enabled", profile);
603+
return;
604+
}
605+
606+
Set<String> profilePlaybooks = new HashSet<>(profileConfig.playbooks());
607+
608+
if (suppliedPlaybooks != null && !suppliedPlaybooks.isEmpty()) {
609+
Set<String> userPlaybooks = new HashSet<>(suppliedPlaybooks);
610+
profilePlaybooks.retainAll(userPlaybooks);
611+
logger.info("Using profile '{}' with user-specified playbooks: {} playbooks",
612+
profile, profilePlaybooks.size());
613+
} else {
614+
logger.info("Using profile '{}': {} playbooks", profile, profilePlaybooks.size());
615+
}
616+
617+
this.suppliedPlaybooks = List.copyOf(profilePlaybooks);
618+
}
619+
564620
/**
565621
* Configures only one playbook to be run.
566622
*
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package dev.dochia.cli.core.args.util;
2+
3+
import io.github.ludovicianul.prettylogger.PrettyLogger;
4+
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
5+
import jakarta.inject.Singleton;
6+
import org.yaml.snakeyaml.Yaml;
7+
8+
import java.io.InputStream;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Optional;
17+
import java.util.Set;
18+
19+
@Singleton
20+
public class ProfileLoader {
21+
private final PrettyLogger logger = PrettyLoggerFactory.getLogger(ProfileLoader.class);
22+
private static final String BUILT_IN_PROFILES = "playbook-profiles.yml";
23+
24+
private Map<String, Profile> profiles;
25+
26+
public ProfileLoader() {
27+
loadBuiltInProfiles();
28+
}
29+
30+
@SuppressWarnings("unchecked")
31+
private void loadBuiltInProfiles() {
32+
try (InputStream is = getClass().getClassLoader().getResourceAsStream(BUILT_IN_PROFILES)) {
33+
34+
Yaml yaml = new Yaml();
35+
Map<String, Object> data = yaml.load(is);
36+
Map<String, Object> profilesData = (Map<String, Object>) data.get("profiles");
37+
38+
profiles = new HashMap<>();
39+
for (Map.Entry<String, Object> entry : profilesData.entrySet()) {
40+
String profileName = entry.getKey();
41+
Map<String, Object> profileData = (Map<String, Object>) entry.getValue();
42+
43+
Profile profile = new Profile(
44+
profileName,
45+
(String) profileData.get("description"),
46+
(List<String>) profileData.getOrDefault("playbooks", Collections.emptyList())
47+
);
48+
profiles.put(profileName, profile);
49+
}
50+
51+
logger.debug("Loaded {} built-in profiles", profiles.size());
52+
} catch (Exception e) {
53+
logger.error("Failed to load built-in profiles", e);
54+
profiles = new HashMap<>();
55+
}
56+
}
57+
58+
@SuppressWarnings("unchecked")
59+
public void loadCustomProfiles(Path customProfileFile) {
60+
try {
61+
Yaml yaml = new Yaml();
62+
Map<String, Object> data = yaml.load(Files.newInputStream(customProfileFile));
63+
Map<String, Object> profilesData = (Map<String, Object>) data.get("profiles");
64+
65+
for (Map.Entry<String, Object> entry : profilesData.entrySet()) {
66+
String profileName = entry.getKey();
67+
Map<String, Object> profileData = (Map<String, Object>) entry.getValue();
68+
69+
Profile profile = new Profile(
70+
profileName,
71+
(String) profileData.get("description"),
72+
(List<String>) profileData.getOrDefault("playbooks", Collections.emptyList())
73+
);
74+
75+
if (profiles.containsKey(profileName)) {
76+
logger.info("Overriding built-in profile: {}", profileName);
77+
}
78+
profiles.put(profileName, profile);
79+
}
80+
81+
logger.debug("Loaded custom profiles from: {}", customProfileFile);
82+
} catch (Exception e) {
83+
logger.error("Failed to load custom profiles from: {}", customProfileFile, e);
84+
}
85+
}
86+
87+
public Optional<Profile> getProfile(String profileName) {
88+
return Optional.ofNullable(profiles.get(profileName));
89+
}
90+
91+
public Set<String> getAvailableProfiles() {
92+
return profiles.keySet();
93+
}
94+
95+
public Collection<Profile> getAvailableProfilesDetails() {
96+
return profiles.values();
97+
}
98+
99+
public record Profile(String name, String description, List<String> playbooks) {
100+
}
101+
}

src/main/java/dev/dochia/cli/core/command/ExplainCommand.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import dev.dochia.cli.core.playbook.special.mutators.api.Mutator;
66
import dev.dochia.cli.core.model.HttpResponse;
77
import dev.dochia.cli.core.model.ResultFactory;
8+
import dev.dochia.cli.core.util.CommonUtils;
89
import dev.dochia.cli.core.util.VersionProvider;
910
import io.github.ludovicianul.prettylogger.PrettyLogger;
1011
import io.github.ludovicianul.prettylogger.PrettyLoggerFactory;
@@ -59,6 +60,7 @@ public class ExplainCommand implements Runnable {
5960

6061
@Override
6162
public void run() {
63+
CommonUtils.initRandom(0);
6264
switch (type) {
6365
case PLAYBOOK -> displayPlaybookInfo();
6466
case MUTATOR -> displayMutatorInfo();

src/main/java/dev/dochia/cli/core/command/ListCommand.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import dev.dochia.cli.core.playbook.special.mutators.api.CustomMutatorConfig;
99
import dev.dochia.cli.core.playbook.special.mutators.api.Mutator;
1010
import dev.dochia.cli.core.util.AnnotationUtils;
11+
import dev.dochia.cli.core.util.CommonUtils;
1112
import dev.dochia.cli.core.util.ConsoleUtils;
1213
import dev.dochia.cli.core.util.JsonUtils;
1314
import dev.dochia.cli.core.util.OpenApiUtils;
@@ -85,6 +86,7 @@ public ListCommand(@Any Instance<TestCasePlaybook> playbooksList, @Any Instance<
8586

8687
@Override
8788
public void run() {
89+
CommonUtils.initRandom(0);
8890
if (listCommandGroups.listPlaybooksGroup != null && listCommandGroups.listPlaybooksGroup.playbooks) {
8991
listPlaybooks();
9092
}

src/main/java/dev/dochia/cli/core/command/TestCommand.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import java.util.concurrent.ExecutorService;
4848
import java.util.concurrent.Executors;
4949
import java.util.concurrent.Future;
50+
import java.util.concurrent.TimeUnit;
5051
import java.util.stream.Collectors;
5152

5253
import static org.fusesource.jansi.Ansi.ansi;
@@ -86,7 +87,7 @@
8687
" dochia test -c openapi.yml -s http://localhost:8080 -H API-Token=$$TOKEN"
8788
})
8889
@Unremovable
89-
public class TestCommand implements Runnable, CommandLine.IExitCodeGenerator {
90+
public class TestCommand implements Runnable, CommandLine.IExitCodeGenerator, AutoCloseable {
9091

9192
private final PrettyLogger logger;
9293
private static final String SEPARATOR = "-".repeat(ConsoleUtils.getConsoleColumns(22));
@@ -220,6 +221,7 @@ void printVersion(Future<VersionChecker.CheckResult> newVersion)
220221
}
221222

222223
private void doLogic() throws IOException {
224+
filterArguments.applyProfile(spec);
223225
this.prepareRun();
224226
OpenAPI openAPI = this.createOpenAPI();
225227
this.checkOpenAPI(openAPI);
@@ -518,4 +520,17 @@ private List<PlaybookData> filterFuzzingData(
518520
public int getExitCode() {
519521
return exitCodeDueToErrors + executionStatisticsListener.getErrors();
520522
}
523+
524+
@Override
525+
public void close() throws Exception {
526+
executor.shutdown();
527+
try {
528+
if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
529+
executor.shutdownNow();
530+
}
531+
} catch (InterruptedException e) {
532+
executor.shutdownNow();
533+
Thread.currentThread().interrupt();
534+
}
535+
}
521536
}

src/main/java/dev/dochia/cli/core/dsl/api/Parser.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public interface Parser {
1919
/** Holds the name for the auth script refresh interval variable. */
2020
String AUTH_REFRESH = "auth_refresh";
2121

22+
String PATH = "path";
23+
2224
/**
2325
* Parses the given expression within the given context and returns the result.
2426
*

0 commit comments

Comments
 (0)