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