diff --git a/src/main/java/org/jboss/jws/diag/common/Severity.java b/src/main/java/org/jboss/jws/diag/common/Severity.java new file mode 100644 index 0000000..2d551fa --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/common/Severity.java @@ -0,0 +1,7 @@ +package org.jboss.jws.diag.common; + +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 new file mode 100644 index 0000000..86e4def --- /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(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..7377d3d --- /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 final 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 e610167..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,9 +2,14 @@ import org.jboss.jws.diag.common.ExitCodes; import org.jboss.jws.diag.common.OutputFormatMixin; +import org.jboss.jws.diag.common.Severity; +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() == Severity.ERROR) { + highestCode = ExitCodes.ERRORS; + } else if (finding.getSeverity() == Severity.WARN && highestCode < ExitCodes.ERRORS) { + highestCode = 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..7d4707c --- /dev/null +++ b/src/main/java/org/jboss/jws/diag/validate/model/Finding.java @@ -0,0 +1,109 @@ +package org.jboss.jws.diag.validate.model; + +import org.jboss.jws.diag.common.Severity; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public final class Finding { + + private final String ruleId; + private final String category; + private final Severity severity; + private final String summary; + private final String detail; + private final String file; + private final String 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() { + return ruleId; + } + public String getCategory() { + return category; + } + public Severity getSeverity() { + return severity; + } + public String getSummary() { + return summary; + } + public String getDetail() { + return detail; + } + public String getFile() { + return file; + } + public String getFix() { + return fix; + } + + @Override + 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 new file mode 100644 index 0000000..66c836d --- /dev/null +++ b/src/test/java/org/jboss/jws/diag/validate/ValidateCommandTest.java @@ -0,0 +1,239 @@ +package org.jboss.jws.diag.validate; + +import org.jboss.jws.diag.common.ExitCodes; +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; + +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 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(findings); + assertThat(result).isEqualTo(ExitCodes.WARNINGS); + } + + @Test + 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(findings); + assertThat(result).isEqualTo(ExitCodes.ERRORS); + } +}