001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.cli.commands; 007 008import gov.nist.secauto.metaschema.cli.processor.CLIProcessor; 009import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext; 010import gov.nist.secauto.metaschema.cli.processor.ExitCode; 011import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor; 012import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 013import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException; 014import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 015import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler; 016import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; 017import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration; 018import gov.nist.secauto.metaschema.core.metapath.MetapathException; 019import gov.nist.secauto.metaschema.core.model.IModule; 020import gov.nist.secauto.metaschema.core.model.MetaschemaException; 021import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationException; 022import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet; 023import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature; 024import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult; 025import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; 026import gov.nist.secauto.metaschema.core.util.IVersionInfo; 027import gov.nist.secauto.metaschema.core.util.ObjectUtils; 028import gov.nist.secauto.metaschema.databind.IBindingContext; 029import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider; 030import gov.nist.secauto.metaschema.databind.io.Format; 031import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 032import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler; 033 034import org.apache.commons.cli.CommandLine; 035import org.apache.commons.cli.Option; 036import org.apache.logging.log4j.LogManager; 037import org.apache.logging.log4j.Logger; 038 039import java.io.FileNotFoundException; 040import java.io.IOException; 041import java.net.URI; 042import java.net.UnknownHostException; 043import java.nio.file.Path; 044import java.nio.file.Paths; 045import java.util.Collection; 046import java.util.List; 047import java.util.Set; 048 049import edu.umd.cs.findbugs.annotations.NonNull; 050import edu.umd.cs.findbugs.annotations.Nullable; 051 052/** 053 * Used by implementing classes to provide a content validation command. 054 */ 055public abstract class AbstractValidateContentCommand 056 extends AbstractTerminalCommand { 057 private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class); 058 @NonNull 059 private static final String COMMAND = "validate"; 060 @NonNull 061 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 062 ExtraArgument.newInstance("file-or-URI-to-validate", true))); 063 064 @NonNull 065 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull( 066 Option.builder("c") 067 .hasArgs() 068 .argName("URL") 069 .desc("additional constraint definitions") 070 .get()); 071 @NonNull 072 private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull( 073 Option.builder("o") 074 .hasArg() 075 .argName("FILE") 076 .desc("write SARIF results to the provided FILE") 077 .numberOfArgs(1) 078 .get()); 079 @NonNull 080 private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull( 081 Option.builder() 082 .longOpt("sarif-include-pass") 083 .desc("include pass results in SARIF") 084 .get()); 085 @NonNull 086 private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull( 087 Option.builder() 088 .longOpt("disable-schema-validation") 089 .desc("do not perform schema validation") 090 .get()); 091 @NonNull 092 private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull( 093 Option.builder() 094 .longOpt("disable-constraint-validation") 095 .desc("do not perform constraint validation") 096 .get()); 097 098 @Override 099 public String getName() { 100 return COMMAND; 101 } 102 103 @SuppressWarnings("null") 104 @Override 105 public Collection<? extends Option> gatherOptions() { 106 return List.of( 107 MetaschemaCommands.AS_FORMAT_OPTION, 108 CONSTRAINTS_OPTION, 109 SARIF_OUTPUT_FILE_OPTION, 110 SARIF_INCLUDE_PASS_OPTION, 111 NO_SCHEMA_VALIDATION_OPTION, 112 NO_CONSTRAINT_VALIDATION_OPTION); 113 } 114 115 @Override 116 public List<ExtraArgument> getExtraArguments() { 117 return EXTRA_ARGUMENTS; 118 } 119 120 /** 121 * Drives the validation execution. 122 */ 123 protected abstract class AbstractValidationCommandExecutor 124 extends AbstractCommandExecutor { 125 126 /** 127 * Construct a new command executor. 128 * 129 * @param callingContext 130 * the context of the command execution 131 * @param commandLine 132 * the parsed command line details 133 */ 134 public AbstractValidationCommandExecutor( 135 @NonNull CallingContext callingContext, 136 @NonNull CommandLine commandLine) { 137 super(callingContext, commandLine); 138 } 139 140 /** 141 * Get the binding context to use for data processing. 142 * 143 * @param constraintSets 144 * the constraints to configure in the resulting binding context 145 * @return the context 146 * @throws CommandExecutionException 147 * if a error occurred while getting the binding context 148 */ 149 @NonNull 150 protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets) 151 throws CommandExecutionException; 152 153 /** 154 * Get the module to use for validation. 155 * <p> 156 * This module is used to generate schemas and as a source of built-in 157 * constraints. 158 * 159 * @param commandLine 160 * the provided command line argument information 161 * @param bindingContext 162 * the context used to access Metaschema module information based on 163 * Java class bindings 164 * @return the loaded Metaschema module 165 * @throws CommandExecutionException 166 * if an error occurred while loading the module 167 */ 168 @NonNull 169 protected abstract IModule getModule( 170 @NonNull CommandLine commandLine, 171 @NonNull IBindingContext bindingContext) 172 throws CommandExecutionException; 173 174 /** 175 * Get the schema validation implementation requested based on the provided 176 * command line arguments. 177 * <p> 178 * It is typical for this call to result in the dynamic generation of a schema 179 * to use for validation. 180 * 181 * @param module 182 * the Metaschema module to generate the schema from 183 * @param commandLine 184 * the provided command line argument information 185 * @param bindingContext 186 * the context used to access Metaschema module information based on 187 * Java class bindings 188 * @return the provider 189 */ 190 @NonNull 191 protected abstract ISchemaValidationProvider getSchemaValidationProvider( 192 @NonNull IModule module, 193 @NonNull CommandLine commandLine, 194 @NonNull IBindingContext bindingContext); 195 196 /** 197 * Execute the validation operation. 198 */ 199 @SuppressWarnings("PMD.OnlyOneReturn") // readability 200 @Override 201 public void execute() throws CommandExecutionException { 202 CommandLine cmdLine = getCommandLine(); 203 @SuppressWarnings("synthetic-access") 204 URI currentWorkingDirectory = ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()); 205 206 Set<IConstraintSet> constraintSets = MetaschemaCommands.loadConstraintSets( 207 cmdLine, 208 CONSTRAINTS_OPTION, 209 currentWorkingDirectory); 210 211 List<String> extraArgs = cmdLine.getArgList(); 212 213 URI source = MetaschemaCommands.handleSource( 214 ObjectUtils.requireNonNull(extraArgs.get(0)), 215 currentWorkingDirectory); 216 217 IBindingContext bindingContext = getBindingContext(constraintSets); 218 IBoundLoader loader = bindingContext.newBoundLoader(); 219 Format asFormat = MetaschemaCommands.determineSourceFormat( 220 cmdLine, 221 MetaschemaCommands.AS_FORMAT_OPTION, 222 loader, 223 source); 224 225 IValidationResult validationResult = validate(source, asFormat, cmdLine, bindingContext); 226 handleOutput(source, validationResult, cmdLine, bindingContext); 227 228 if (validationResult == null || validationResult.isPassing()) { 229 if (LOGGER.isInfoEnabled()) { 230 LOGGER.info("The file '{}' is valid.", source); 231 } 232 } else if (LOGGER.isErrorEnabled()) { 233 LOGGER.error("The file '{}' is invalid.", source); 234 } 235 236 if (validationResult != null && !validationResult.isPassing()) { 237 throw new CommandExecutionException(ExitCode.FAIL); 238 } 239 } 240 241 @SuppressWarnings("PMD.CyclomaticComplexity") 242 @Nullable 243 private IValidationResult validate( 244 @NonNull URI source, 245 @NonNull Format asFormat, 246 @NonNull CommandLine commandLine, 247 @NonNull IBindingContext bindingContext) throws CommandExecutionException { 248 249 if (LOGGER.isInfoEnabled()) { 250 LOGGER.info("Validating '{}' as {}.", source, asFormat.name()); 251 } 252 253 IValidationResult validationResult = null; 254 try { 255 // get the module, but don't register it 256 IModule module = getModule(commandLine, bindingContext); 257 if (!commandLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) { 258 // perform schema validation 259 validationResult = getSchemaValidationProvider(module, commandLine, bindingContext) 260 .validateWithSchema(source, asFormat, bindingContext); 261 } 262 263 if (!commandLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION)) { 264 IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>(); 265 if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && commandLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) { 266 configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS); 267 } 268 269 // perform constraint validation 270 bindingContext.registerModule(module); // ensure the module is registered 271 IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration); 272 validationResult = validationResult == null 273 ? constraintValidationResult 274 : AggregateValidationResult.aggregate(validationResult, constraintValidationResult); 275 } 276 } catch (FileNotFoundException ex) { 277 throw new CommandExecutionException( 278 ExitCode.IO_ERROR, 279 String.format("Resource not found at '%s'", source), 280 ex); 281 } catch (UnknownHostException ex) { 282 throw new CommandExecutionException( 283 ExitCode.IO_ERROR, 284 String.format("Unknown host for '%s'.", source), 285 ex); 286 } catch (IOException ex) { 287 throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex); 288 } catch (MetapathException | MetaschemaException | ConstraintValidationException ex) { 289 throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex.getLocalizedMessage(), ex); 290 } 291 return validationResult; 292 } 293 294 private void handleOutput( 295 @NonNull URI source, 296 @Nullable IValidationResult validationResult, 297 @NonNull CommandLine commandLine, 298 @NonNull IBindingContext bindingContext) throws CommandExecutionException { 299 if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION)) { 300 Path sarifFile = ObjectUtils.notNull(Paths.get(commandLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION))); 301 302 IVersionInfo version 303 = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION); 304 305 try { 306 SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version); 307 if (validationResult != null) { 308 sarifHandler.addFindings(validationResult.getFindings()); 309 } 310 sarifHandler.write(sarifFile, bindingContext); 311 } catch (IOException ex) { 312 throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex); 313 } 314 } else if (validationResult != null && !validationResult.getFindings().isEmpty()) { 315 LOGGER.info("Validation identified the following issues:"); 316 LoggingValidationHandler.instance().handleResults(validationResult); 317 } 318 319 } 320 } 321}