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