From 28780aaedc64ee1e2f7a65197789fe2c7e355c2e Mon Sep 17 00:00:00 2001 From: Milan Samuel Date: Wed, 27 May 2026 22:31:47 +0530 Subject: [PATCH 1/3] Add rules engine framework and exit code validation Rules Engine Framework: Establishes the core architecture and interfaces to execute diagnostic checks across the environment. Exit Code Validation: Integrates logic within ValidateCommand to map aggregated findings directly to system exit codes. --- .../jboss/jws/diag/common/SeverityLevels.java | 7 +++ .../org/jboss/jws/diag/validate/Rule.java | 10 ++++ .../jws/diag/validate/ValidateCommand.java | 25 +++++++++- .../jws/diag/validate/model/Finding.java | 47 +++++++++++++++++++ .../diag/validate/ValidateCommandTest.java | 45 ++++++++++++++++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/jboss/jws/diag/common/SeverityLevels.java create mode 100644 src/main/java/org/jboss/jws/diag/validate/Rule.java create mode 100644 src/main/java/org/jboss/jws/diag/validate/model/Finding.java create mode 100644 src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java diff --git a/src/main/java/org/jboss/jws/diag/common/SeverityLevels.java b/src/main/java/org/jboss/jws/diag/common/SeverityLevels.java new file mode 100644 index 0000000..292f1aa --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/common/SeverityLevels.java @@ -0,0 +1,7 @@ +package org.jboss.jws.diag.common; + +public enum SeverityLevels { + ERROR, + WARN, + INFO +} diff --git a/src/main/java/org/jboss/jws/diag/validate/Rule.java b/src/main/java/org/jboss/jws/diag/validate/Rule.java new file mode 100644 index 0000000..6eab7ca --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/validate/Rule.java @@ -0,0 +1,10 @@ +package org.jboss.jws.diag.validate; + +import org.jboss.jws.diag.validate.model.Finding; + +import java.util.List; + +public interface Rule { + + List evaluate(); +} diff --git a/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java b/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java index e610167..4581a6a 100644 --- a/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java +++ b/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java @@ -2,9 +2,14 @@ import org.jboss.jws.diag.common.ExitCodes; import org.jboss.jws.diag.common.OutputFormatMixin; +import org.jboss.jws.diag.common.SeverityLevels; +import org.jboss.jws.diag.validate.model.Finding; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; +import java.util.ArrayList; +import java.util.List; + @Command(name = "validate", description = "Run diagnostic rules against configuration and report findings (INFO/WARN/ERROR)", mixinStandardHelpOptions = true) @@ -15,7 +20,23 @@ public class ValidateCommand implements Runnable { @Override public void run() { - System.out.println("jws-diag validate: not yet implemented"); - System.exit(ExitCodes.OK); + List findings = new ArrayList<>(); + + int exitCode = determineExitCode(findings); + System.exit(exitCode); + } + + public int determineExitCode(List findings) { + int highestCode = ExitCodes.OK; + + for (Finding finding : findings) { + if (finding.getSeverity() == SeverityLevels.ERROR) { + return ExitCodes.ERRORS; + } else if (finding.getSeverity() == SeverityLevels.WARN) { + return ExitCodes.WARNINGS; + } + } + + return highestCode; } } diff --git a/src/main/java/org/jboss/jws/diag/validate/model/Finding.java b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java new file mode 100644 index 0000000..9597124 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java @@ -0,0 +1,47 @@ +package org.jboss.jws.diag.validate.model; + +import org.jboss.jws.diag.common.SeverityLevels; + +public class Finding { + private final String ruleId; + private final String category; + private final SeverityLevels severityLevels; + private final String summary; + private final String detail; + private final String file; + private final String fix; + + public Finding(String ruleId, String category, SeverityLevels severityLevels, + String summary, String detail, String file, String fix) + { + this.ruleId = ruleId; + this.category = category; + this.severityLevels = severityLevels; + this.summary = summary; + this.detail = detail; + this.file = file; + this.fix = fix; + } + + public String getRuleId() { + return ruleId; + } + public String getCategory() { + return category; + } + public SeverityLevels getSeverity() { + return severityLevels; + } + public String getSummary() { + return summary; + } + public String getDetail() { + return detail; + } + public String getFile() { + return file; + } + public String getFix() { + return fix; + } +} diff --git a/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java b/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java new file mode 100644 index 0000000..70993b5 --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java @@ -0,0 +1,45 @@ +package org.jboss.jws.diag.validate; + +import org.jboss.jws.diag.common.ExitCodes; +import org.jboss.jws.diag.common.SeverityLevels; + +import org.jboss.jws.diag.validate.model.Finding; +import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ValidateCommandTest { + + private final ValidateCommand validateCommand = new ValidateCommand(); + + @Test + void shouldReturnOkWhenNoFindingsArePresent() { + int result = validateCommand.determineExitCode(Collections.emptyList()); + assertThat(result).isEqualTo(ExitCodes.OK); + } + + @Test + void shouldReturnWarningWhenFindingsContainOnlyWarnings() { + List warningFindings = List.of( + new Finding("CONN-001", "Connector", SeverityLevels.WARN, "Low threads check", "Compares maxThreads against available CPU cores rather than using a rigid static number" + , "server.xml", "Adjust maxThreads upward to match your host hardware specifications.") + ); + + int result = validateCommand.determineExitCode(warningFindings); + assertThat(result).isEqualTo(ExitCodes.WARNINGS); + } + + @Test + void shouldReturnErrorWhenAnyFindingsHasError() { + List errorFindings = List.of( + new Finding("SEC-001", "Security", SeverityLevels.ERROR, "Root user check" + , "Checks if the Tomcat process is running as root (UID 0)", "process state" + , "Run Tomcat as a dedicated, non-root system user.") + ); + + int result = validateCommand.determineExitCode(errorFindings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } +} From ed0837087e6c7ff102f4b4829ab61dc1c69c2778 Mon Sep 17 00:00:00 2001 From: Milan Samuel Date: Fri, 29 May 2026 16:23:21 +0530 Subject: [PATCH 2/3] Refactor Severity, use Builder, expand tests, and fix exit logic --- .../{SeverityLevels.java => Severity.java} | 2 +- .../org/jboss/jws/diag/validate/Rule.java | 2 +- .../jboss/jws/diag/validate/RuleContext.java | 15 ++ .../jws/diag/validate/ValidateCommand.java | 10 +- .../jws/diag/validate/model/Finding.java | 93 ++++++-- .../diag/validate/ValidateCommandTest.java | 216 +++++++++++++++++- 6 files changed, 304 insertions(+), 34 deletions(-) rename src/main/java/org/jboss/jws/diag/common/{SeverityLevels.java => Severity.java} (70%) create mode 100644 src/main/java/org/jboss/jws/diag/validate/RuleContext.java diff --git a/src/main/java/org/jboss/jws/diag/common/SeverityLevels.java b/src/main/java/org/jboss/jws/diag/common/Severity.java similarity index 70% rename from src/main/java/org/jboss/jws/diag/common/SeverityLevels.java rename to src/main/java/org/jboss/jws/diag/common/Severity.java index 292f1aa..2d551fa 100644 --- a/src/main/java/org/jboss/jws/diag/common/SeverityLevels.java +++ b/src/main/java/org/jboss/jws/diag/common/Severity.java @@ -1,6 +1,6 @@ package org.jboss.jws.diag.common; -public enum SeverityLevels { +public enum Severity { ERROR, WARN, INFO diff --git a/src/main/java/org/jboss/jws/diag/validate/Rule.java b/src/main/java/org/jboss/jws/diag/validate/Rule.java index 6eab7ca..86e4def 100644 --- a/src/main/java/org/jboss/jws/diag/validate/Rule.java +++ b/src/main/java/org/jboss/jws/diag/validate/Rule.java @@ -6,5 +6,5 @@ public interface Rule { - List evaluate(); + List evaluate(RuleContext context); } diff --git a/src/main/java/org/jboss/jws/diag/validate/RuleContext.java b/src/main/java/org/jboss/jws/diag/validate/RuleContext.java new file mode 100644 index 0000000..f20e6f8 --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/validate/RuleContext.java @@ -0,0 +1,15 @@ +package org.jboss.jws.diag.validate; + +import java.nio.file.Path; + +public class RuleContext { + private final Path catalinaBase; + + public RuleContext(Path catalinaBase) { + this.catalinaBase = catalinaBase; + } + + public Path getCatalinaBase() { + return catalinaBase; + } +} diff --git a/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java b/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java index 4581a6a..fdec784 100644 --- a/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java +++ b/src/main/java/org/jboss/jws/diag/validate/ValidateCommand.java @@ -2,7 +2,7 @@ import org.jboss.jws.diag.common.ExitCodes; import org.jboss.jws.diag.common.OutputFormatMixin; -import org.jboss.jws.diag.common.SeverityLevels; +import org.jboss.jws.diag.common.Severity; import org.jboss.jws.diag.validate.model.Finding; import picocli.CommandLine.Command; import picocli.CommandLine.Mixin; @@ -30,10 +30,10 @@ public int determineExitCode(List findings) { int highestCode = ExitCodes.OK; for (Finding finding : findings) { - if (finding.getSeverity() == SeverityLevels.ERROR) { - return ExitCodes.ERRORS; - } else if (finding.getSeverity() == SeverityLevels.WARN) { - return ExitCodes.WARNINGS; + if (finding.getSeverity() == Severity.ERROR) { + highestCode = ExitCodes.ERRORS; + } else if (finding.getSeverity() == Severity.WARN && highestCode < ExitCodes.ERRORS) { + highestCode = ExitCodes.WARNINGS; } } diff --git a/src/main/java/org/jboss/jws/diag/validate/model/Finding.java b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java index 9597124..77e2476 100644 --- a/src/main/java/org/jboss/jws/diag/validate/model/Finding.java +++ b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java @@ -1,26 +1,75 @@ package org.jboss.jws.diag.validate.model; -import org.jboss.jws.diag.common.SeverityLevels; +import org.jboss.jws.diag.common.Severity; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Finding { -public class Finding { private final String ruleId; private final String category; - private final SeverityLevels severityLevels; + private final Severity severity; private final String summary; private final String detail; private final String file; private final String fix; - public Finding(String ruleId, String category, SeverityLevels severityLevels, - String summary, String detail, String file, String fix) - { - this.ruleId = ruleId; - this.category = category; - this.severityLevels = severityLevels; - this.summary = summary; - this.detail = detail; - this.file = file; - this.fix = fix; + private Finding(Builder builder) { + this.ruleId = builder.ruleId; + this.category = builder.category; + this.severity = builder.severity; + this.summary = builder.summary; + this.detail = builder.detail; + this.file = builder.file; + this.fix = builder.fix; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder{ + + private String ruleId; + private String category; + private Severity severity; + private String summary; + private String detail; + private String file; + private String fix; + + public Builder ruleId(String ruleId) { + this.ruleId = ruleId; + return this; + } + public Builder category(String category) { + this.category = category; + return this; + } + public Builder severity(Severity severity) { + this.severity = severity; + return this; + } + public Builder summary(String summary) { + this.summary = summary; + return this; + } + public Builder detail(String detail) { + this.detail = detail; + return this; + } + public Builder file(String file) { + this.file = file; + return this; + } + public Builder fix(String fix) { + this.fix = fix; + return this; + } + + public Finding build() { + return new Finding(this); + } } public String getRuleId() { @@ -29,8 +78,8 @@ public String getRuleId() { public String getCategory() { return category; } - public SeverityLevels getSeverity() { - return severityLevels; + public Severity getSeverity() { + return severity; } public String getSummary() { return summary; @@ -44,4 +93,16 @@ public String getFile() { public String getFix() { return fix; } -} + + public String toString() { + return "Finding{" + + "ruleId='" + ruleId + '\'' + + ", category='" + category + '\'' + + ", severity='" + severity + '\'' + + ", summary='" + summary + '\'' + + ", detail='" + detail + '\'' + + ", file='" + file + '\'' + + ", fix='" + fix + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java b/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java index 70993b5..66c836d 100644 --- a/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java +++ b/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java @@ -1,10 +1,11 @@ package org.jboss.jws.diag.validate; import org.jboss.jws.diag.common.ExitCodes; -import org.jboss.jws.diag.common.SeverityLevels; +import org.jboss.jws.diag.common.Severity; import org.jboss.jws.diag.validate.model.Finding; import org.junit.jupiter.api.Test; + import java.util.Collections; import java.util.List; @@ -22,24 +23,217 @@ void shouldReturnOkWhenNoFindingsArePresent() { @Test void shouldReturnWarningWhenFindingsContainOnlyWarnings() { - List warningFindings = List.of( - new Finding("CONN-001", "Connector", SeverityLevels.WARN, "Low threads check", "Compares maxThreads against available CPU cores rather than using a rigid static number" - , "server.xml", "Adjust maxThreads upward to match your host hardware specifications.") + List findings = List.of( + Finding.builder() + .ruleId("SEC-004") + .category("Security") + .severity(Severity.WARN) + .summary("Version Banner Exposure Check") + .detail("Checks if elements expose server metadata, or if an is missing inside the or blocks to suppress versions on error pages") + .file("server.xml") + .fix("Configure an with showReport=\"false\" and showServerInfo=\"false\" inside your Host block") + .build() ); - int result = validateCommand.determineExitCode(warningFindings); + int result = validateCommand.determineExitCode(findings); assertThat(result).isEqualTo(ExitCodes.WARNINGS); } @Test - void shouldReturnErrorWhenAnyFindingsHasError() { - List errorFindings = List.of( - new Finding("SEC-001", "Security", SeverityLevels.ERROR, "Root user check" - , "Checks if the Tomcat process is running as root (UID 0)", "process state" - , "Run Tomcat as a dedicated, non-root system user.") + void shouldReturnErrorWhenFindingsContainsOnlyErrors() { + List findings = List.of( + Finding.builder() + .ruleId("SEC-001") + .category("Security") + .severity(Severity.ERROR) + .summary("Root User Check") + .detail("Checks if the Tomcat process is running as root (UID 0)") + .file("Process State") + .fix("Run Tomcat as a dedicated, non-root system user") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } + + @Test + void shouldReturnErrorWhenFindingsContainWarnAndError() { + List findings = List.of( + Finding.builder() + .ruleId("SEC-004") + .category("Security") + .severity(Severity.WARN) + .summary("Version Banner Exposure Check") + .detail("Checks if elements expose server metadata, or if an is missing inside the or blocks to suppress versions on error pages") + .file("server.xml") + .fix("Configure an with showReport=\"false\" and showServerInfo=\"false\" inside your Host block") + .build(), + Finding.builder() + .ruleId("SEC-001") + .category("Security") + .severity(Severity.ERROR) + .summary("Root User Check") + .detail("Checks if the Tomcat process is running as root (UID 0)") + .file("Process State") + .fix("Run Tomcat as a dedicated, non-root system user") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } + + @Test + void shouldReturnErrorWhenFindingsContainErrorAndWarn() { + List findings = List.of( + Finding.builder() + .ruleId("SEC-001") + .category("Security") + .severity(Severity.ERROR) + .summary("Root User Check") + .detail("Checks if the Tomcat process is running as root (UID 0)") + .file("Process State") + .fix("Run Tomcat as a dedicated, non-root system user") + .build(), + Finding.builder() + .ruleId("SEC-004") + .category("Security") + .severity(Severity.WARN) + .summary("Version Banner Exposure Check") + .detail("Checks if elements expose server metadata, or if an is missing inside the or blocks to suppress versions on error pages") + .file("server.xml") + .fix("Configure an with showReport=\"false\" and showServerInfo=\"false\" inside your Host block") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } + + @Test + void shouldReturnOkWhenFindingsContainOnlyInfo() { + List findings = List.of( + Finding.builder() + .ruleId("CONN-004") + .category("Connector") + .severity(Severity.INFO) + .summary("Missing Redirect Port") + .detail("Inspects whether standard HTTP connectors omit the redirectPort attribute.") + .file("server.xml") + .fix("Add redirectPort=\"8443\" to allow automatic HTTPS redirection fields.") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.OK); + } + + @Test + void shouldReturnWarningWhenFindingsContainInfoAndWarn() { + List findings = List.of( + Finding.builder() + .ruleId("CONN-004") + .category("Connector") + .severity(Severity.INFO) + .summary("Missing Redirect Port") + .detail("Inspects whether standard HTTP connectors omit the redirectPort attribute.") + .file("server.xml") + .fix("Add redirectPort=\"8443\" to allow automatic HTTPS redirection fields.") + .build(), + Finding.builder() + .ruleId("SEC-004") + .category("Security") + .severity(Severity.WARN) + .summary("Version Banner Exposure Check") + .detail("Checks if elements expose server metadata, or if an is missing inside the or blocks to suppress versions on error pages") + .file("server.xml") + .fix("Configure an with showReport=\"false\" and showServerInfo=\"false\" inside your Host block") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.WARNINGS); + } + + @Test + void shouldReturnWarningWhenFindingsContainWarnAndInfo() { + List findings = List.of( + Finding.builder() + .ruleId("SEC-004") + .category("Security") + .severity(Severity.WARN) + .summary("Version Banner Exposure Check") + .detail("Checks if elements expose server metadata, or if an is missing inside the or blocks to suppress versions on error pages") + .file("server.xml") + .fix("Configure an with showReport=\"false\" and showServerInfo=\"false\" inside your Host block") + .build(), + Finding.builder() + .ruleId("CONN-004") + .category("Connector") + .severity(Severity.INFO) + .summary("Missing Redirect Port") + .detail("Inspects whether standard HTTP connectors omit the redirectPort attribute.") + .file("server.xml") + .fix("Add redirectPort=\"8443\" to allow automatic HTTPS redirection fields.") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.WARNINGS); + } + + @Test + void shouldReturnErrorWhenFindingsContainInfoAndError() { + List findings = List.of( + Finding.builder() + .ruleId("CONN-004") + .category("Connector") + .severity(Severity.INFO) + .summary("Missing Redirect Port") + .detail("Inspects whether standard HTTP connectors omit the redirectPort attribute.") + .file("server.xml") + .fix("Add redirectPort=\"8443\" to allow automatic HTTPS redirection fields.") + .build(), + Finding.builder() + .ruleId("SEC-001") + .category("Security") + .severity(Severity.ERROR) + .summary("Root User Check") + .detail("Checks if the Tomcat process is running as root (UID 0)") + .file("Process State") + .fix("Run Tomcat as a dedicated, non-root system user") + .build() + ); + + int result = validateCommand.determineExitCode(findings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } + + @Test + void shouldReturnErrorWhenFindingsContainErrorAndInfo() { + List findings = List.of( + Finding.builder() + .ruleId("SEC-001") + .category("Security") + .severity(Severity.ERROR) + .summary("Root User Check") + .detail("Checks if the Tomcat process is running as root (UID 0)") + .file("Process State") + .fix("Run Tomcat as a dedicated, non-root system user") + .build(), + Finding.builder() + .ruleId("CONN-004") + .category("Connector") + .severity(Severity.INFO) + .summary("Missing Redirect Port") + .detail("Inspects whether standard HTTP connectors omit the redirectPort attribute.") + .file("server.xml") + .fix("Add redirectPort=\"8443\" to allow automatic HTTPS redirection fields.") + .build() ); - int result = validateCommand.determineExitCode(errorFindings); + int result = validateCommand.determineExitCode(findings); assertThat(result).isEqualTo(ExitCodes.ERRORS); } } From f52cd5ed2bcead543640e428e2ce07f9a7956c90 Mon Sep 17 00:00:00 2001 From: Milan Samuel Date: Fri, 29 May 2026 20:02:31 +0530 Subject: [PATCH 3/3] Add @Override annotation, space fix and mark RuleContext as final --- src/main/java/org/jboss/jws/diag/validate/RuleContext.java | 2 +- src/main/java/org/jboss/jws/diag/validate/model/Finding.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jboss/jws/diag/validate/RuleContext.java b/src/main/java/org/jboss/jws/diag/validate/RuleContext.java index f20e6f8..7377d3d 100644 --- a/src/main/java/org/jboss/jws/diag/validate/RuleContext.java +++ b/src/main/java/org/jboss/jws/diag/validate/RuleContext.java @@ -2,7 +2,7 @@ import java.nio.file.Path; -public class RuleContext { +public final class RuleContext { private final Path catalinaBase; public RuleContext(Path catalinaBase) { diff --git a/src/main/java/org/jboss/jws/diag/validate/model/Finding.java b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java index 77e2476..7d4707c 100644 --- a/src/main/java/org/jboss/jws/diag/validate/model/Finding.java +++ b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java @@ -28,7 +28,7 @@ public static Builder builder() { return new Builder(); } - public static final class Builder{ + public static final class Builder { private String ruleId; private String category; @@ -94,6 +94,7 @@ public String getFix() { return fix; } + @Override public String toString() { return "Finding{" + "ruleId='" + ruleId + '\'' +