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 @SuppressWarnings("synthetic-access") 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 // get the module, but don't register it 254 IModule module = getModule(commandLine, bindingContext); 255 if (!commandLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) { 256 // perform schema validation 257 validationResult = getSchemaValidationProvider(module, commandLine, bindingContext) 258 .validateWithSchema(source, asFormat, bindingContext); 259 } 260 261 if (!commandLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION)) { 262 IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>(); 263 if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && commandLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) { 264 configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS); 265 } 266 267 // perform constraint validation 268 bindingContext.registerModule(module); // ensure the module is registered 269 IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration); 270 validationResult = validationResult == null 271 ? constraintValidationResult 272 : AggregateValidationResult.aggregate(validationResult, constraintValidationResult); 273 } 274 } catch (FileNotFoundException ex) { 275 throw new CommandExecutionException( 276 ExitCode.IO_ERROR, 277 String.format("Resource not found at '%s'", source), 278 ex); 279 } catch (UnknownHostException ex) { 280 throw new CommandExecutionException( 281 ExitCode.IO_ERROR, 282 String.format("Unknown host for '%s'.", source), 283 ex); 284 } catch (IOException ex) { 285 throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex); 286 } catch (MetapathException ex) { 287 throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex.getLocalizedMessage(), ex); 288 } 289 return validationResult; 290 } 291 292 private void handleOutput( 293 @NonNull URI source, 294 @Nullable IValidationResult validationResult, 295 @NonNull CommandLine commandLine, 296 @NonNull IBindingContext bindingContext) throws CommandExecutionException { 297 if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION)) { 298 Path sarifFile = ObjectUtils.notNull(Paths.get(commandLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION))); 299 300 IVersionInfo version 301 = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION); 302 303 try { 304 SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version); 305 if (validationResult != null) { 306 sarifHandler.addFindings(validationResult.getFindings()); 307 } 308 sarifHandler.write(sarifFile, bindingContext); 309 } catch (IOException ex) { 310 throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex); 311 } 312 } else if (validationResult != null && !validationResult.getFindings().isEmpty()) { 313 LOGGER.info("Validation identified the following issues:"); 314 LoggingValidationHandler.instance().handleResults(validationResult); 315 } 316 317 } 318 } 319}