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.ExitStatus; 012import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException; 013import gov.nist.secauto.metaschema.cli.processor.OptionUtils; 014import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor; 015import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand; 016import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument; 017import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 018import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler; 019import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration; 020import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration; 021import gov.nist.secauto.metaschema.core.metapath.MetapathException; 022import gov.nist.secauto.metaschema.core.model.IConstraintLoader; 023import gov.nist.secauto.metaschema.core.model.MetaschemaException; 024import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet; 025import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature; 026import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult; 027import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; 028import gov.nist.secauto.metaschema.core.util.CollectionUtil; 029import gov.nist.secauto.metaschema.core.util.CustomCollectors; 030import gov.nist.secauto.metaschema.core.util.IVersionInfo; 031import gov.nist.secauto.metaschema.core.util.ObjectUtils; 032import gov.nist.secauto.metaschema.core.util.UriUtils; 033import gov.nist.secauto.metaschema.databind.IBindingContext; 034import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider; 035import gov.nist.secauto.metaschema.databind.io.Format; 036import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 037import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader; 038import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler; 039 040import org.apache.commons.cli.CommandLine; 041import org.apache.commons.cli.Option; 042import org.apache.logging.log4j.LogManager; 043import org.apache.logging.log4j.Logger; 044 045import java.io.FileNotFoundException; 046import java.io.IOException; 047import java.net.URI; 048import java.net.URISyntaxException; 049import java.net.UnknownHostException; 050import java.nio.file.Path; 051import java.nio.file.Paths; 052import java.util.Arrays; 053import java.util.Collection; 054import java.util.LinkedHashSet; 055import java.util.List; 056import java.util.Locale; 057import java.util.Set; 058 059import edu.umd.cs.findbugs.annotations.NonNull; 060 061public abstract class AbstractValidateContentCommand 062 extends AbstractTerminalCommand { 063 private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class); 064 @NonNull 065 private static final String COMMAND = "validate"; 066 @NonNull 067 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 068 new DefaultExtraArgument("file-or-URI-to-validate", true))); 069 070 @NonNull 071 private static final Option AS_OPTION = ObjectUtils.notNull( 072 Option.builder() 073 .longOpt("as") 074 .hasArg() 075 .argName("FORMAT") 076 .desc("source format: xml, json, or yaml") 077 .numberOfArgs(1) 078 .build()); 079 @NonNull 080 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull( 081 Option.builder("c") 082 .hasArgs() 083 .argName("URL") 084 .desc("additional constraint definitions") 085 .build()); 086 @NonNull 087 private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull( 088 Option.builder("o") 089 .hasArg() 090 .argName("FILE") 091 .desc("write SARIF results to the provided FILE") 092 .numberOfArgs(1) 093 .build()); 094 @NonNull 095 private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull( 096 Option.builder() 097 .longOpt("sarif-include-pass") 098 .desc("include pass results in SARIF") 099 .build()); 100 @NonNull 101 private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull( 102 Option.builder() 103 .longOpt("disable-schema-validation") 104 .desc("do not perform schema validation") 105 .build()); 106 @NonNull 107 private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull( 108 Option.builder() 109 .longOpt("disable-constraint-validation") 110 .desc("do not perform constraint validation") 111 .build()); 112 113 @Override 114 public String getName() { 115 return COMMAND; 116 } 117 118 @SuppressWarnings("null") 119 @Override 120 public Collection<? extends Option> gatherOptions() { 121 return List.of( 122 AS_OPTION, 123 CONSTRAINTS_OPTION, 124 SARIF_OUTPUT_FILE_OPTION, 125 SARIF_INCLUDE_PASS_OPTION, 126 NO_SCHEMA_VALIDATION_OPTION, 127 NO_CONSTRAINT_VALIDATION_OPTION); 128 } 129 130 @Override 131 public List<ExtraArgument> getExtraArguments() { 132 return EXTRA_ARGUMENTS; 133 } 134 135 @SuppressWarnings("PMD.PreserveStackTrace") // intended 136 @Override 137 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException { 138 List<String> extraArgs = cmdLine.getArgList(); 139 if (extraArgs.size() != 1) { 140 throw new InvalidArgumentException("The source to validate must be provided."); 141 } 142 143 if (cmdLine.hasOption(AS_OPTION)) { 144 try { 145 String toFormatText = cmdLine.getOptionValue(AS_OPTION); 146 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 147 } catch (IllegalArgumentException ex) { 148 InvalidArgumentException newEx = new InvalidArgumentException( 149 String.format("Invalid '%s' argument. The format must be one of: %s.", 150 OptionUtils.toArgument(AS_OPTION), 151 Arrays.asList(Format.values()).stream() 152 .map(format -> format.name()) 153 .collect(CustomCollectors.joiningWithOxfordComma("and")))); 154 newEx.addSuppressed(ex); 155 throw newEx; 156 } 157 } 158 } 159 160 protected abstract class AbstractValidationCommandExecutor 161 extends AbstractCommandExecutor 162 implements ISchemaValidationProvider { 163 164 /** 165 * Construct a new command executor. 166 * 167 * @param callingContext 168 * the context of the command execution 169 * @param commandLine 170 * the parsed command line details 171 */ 172 public AbstractValidationCommandExecutor( 173 @NonNull CallingContext callingContext, 174 @NonNull CommandLine commandLine) { 175 super(callingContext, commandLine); 176 } 177 178 /** 179 * Get the binding context to use for data processing. 180 * 181 * @param constraintSets 182 * the constraints to configure in the resulting binding context 183 * @return the context 184 * @throws MetaschemaException 185 * if a Metaschema error occurred 186 * @throws IOException 187 * if an error occurred while reading data 188 */ 189 @NonNull 190 protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets) 191 throws MetaschemaException, IOException; 192 193 @SuppressWarnings("PMD.OnlyOneReturn") // readability 194 @Override 195 public ExitStatus execute() { 196 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri()); 197 CommandLine cmdLine = getCommandLine(); 198 199 Set<IConstraintSet> constraintSets; 200 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) { 201 IConstraintLoader constraintLoader = new BindingConstraintLoader(IBindingContext.instance()); 202 constraintSets = new LinkedHashSet<>(); 203 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION); 204 for (String arg : args) { 205 assert arg != null; 206 try { 207 URI constraintUri = ObjectUtils.requireNonNull(UriUtils.toUri(arg, cwd)); 208 constraintSets.addAll(constraintLoader.load(constraintUri)); 209 } catch (IOException | MetaschemaException | MetapathException | URISyntaxException ex) { 210 return ExitCode.IO_ERROR.exitMessage("Unable to load constraint set '" + arg + "'.").withThrowable(ex); 211 } 212 } 213 } else { 214 constraintSets = CollectionUtil.emptySet(); 215 } 216 217 IBindingContext bindingContext; 218 try { 219 bindingContext = getBindingContext(constraintSets); 220 } catch (IOException | MetaschemaException ex) { 221 return ExitCode.PROCESSING_ERROR 222 .exitMessage("Unable to get binding context. " + ex.getMessage()) 223 .withThrowable(ex); 224 } 225 226 IBoundLoader loader = bindingContext.newBoundLoader(); 227 228 List<String> extraArgs = cmdLine.getArgList(); 229 230 String sourceName = ObjectUtils.requireNonNull(extraArgs.get(0)); 231 URI source; 232 233 try { 234 source = UriUtils.toUri(sourceName, cwd); 235 } catch (URISyntaxException ex) { 236 return ExitCode.IO_ERROR.exitMessage("Cannot load source '%s' as it is not a valid file or URI.") 237 .withThrowable(ex); 238 } 239 240 Format asFormat; 241 if (cmdLine.hasOption(AS_OPTION)) { 242 try { 243 String toFormatText = cmdLine.getOptionValue(AS_OPTION); 244 asFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT)); 245 } catch (IllegalArgumentException ex) { 246 return ExitCode.IO_ERROR 247 .exitMessage("Invalid '--as' argument. The format must be one of: " 248 + Arrays.stream(Format.values()) 249 .map(format -> format.name()) 250 .collect(CustomCollectors.joiningWithOxfordComma("or"))) 251 .withThrowable(ex); 252 } 253 } else { 254 // attempt to determine the format 255 try { 256 asFormat = loader.detectFormat(source); 257 } catch (FileNotFoundException ex) { 258 // this case was already checked for 259 return ExitCode.IO_ERROR.exitMessage("The provided source file '" + source + "' does not exist."); 260 } catch (IOException ex) { 261 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); 262 } catch (IllegalArgumentException ex) { 263 return ExitCode.IO_ERROR.exitMessage( 264 "Source file has unrecognizable format. Use '--as' to specify the format. The format must be one of: " 265 + Arrays.stream(Format.values()) 266 .map(format -> format.name()) 267 .collect(CustomCollectors.joiningWithOxfordComma("or"))); 268 } 269 } 270 271 if (LOGGER.isInfoEnabled()) { 272 LOGGER.info("Validating '{}' as {}.", source, asFormat.name()); 273 } 274 275 IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>(); 276 if (cmdLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && cmdLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) { 277 configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS); 278 } 279 280 IValidationResult validationResult = null; 281 try { 282 if (!cmdLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) { 283 // perform schema validation 284 validationResult = this.validateWithSchema(source, asFormat); 285 } 286 287 if (!cmdLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION) 288 && (validationResult == null || validationResult.isPassing())) { 289 // perform constraint validation 290 IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration); 291 validationResult = validationResult == null 292 ? constraintValidationResult 293 : AggregateValidationResult.aggregate(validationResult, constraintValidationResult); 294 } 295 } catch (FileNotFoundException ex) { 296 return ExitCode.IO_ERROR.exitMessage(String.format("Resource not found at '%s'", source)).withThrowable(ex); 297 } catch (UnknownHostException ex) { 298 return ExitCode.IO_ERROR.exitMessage(String.format("Unknown host for '%s'.", source)).withThrowable(ex); 299 } catch (IOException ex) { 300 return ExitCode.IO_ERROR.exit().withThrowable(ex); 301 } catch (MetapathException ex) { 302 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); 303 } 304 305 if (cmdLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) { 306 Path sarifFile = ObjectUtils.notNull(Paths.get(cmdLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION))); 307 308 IVersionInfo version 309 = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION); 310 311 try { 312 SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version); 313 if (validationResult != null) { 314 sarifHandler.addFindings(validationResult.getFindings()); 315 } 316 sarifHandler.write(sarifFile); 317 } catch (IOException ex) { 318 return ExitCode.IO_ERROR.exit().withThrowable(ex); 319 } 320 } else if (validationResult != null && !validationResult.getFindings().isEmpty()) { 321 LOGGER.info("Validation identified the following issues:", source); 322 LoggingValidationHandler.instance().handleValidationResults(validationResult); 323 } 324 325 if (validationResult == null || validationResult.isPassing()) { 326 if (LOGGER.isInfoEnabled()) { 327 LOGGER.info("The file '{}' is valid.", source); 328 } 329 } else if (LOGGER.isErrorEnabled()) { 330 LOGGER.error("The file '{}' is invalid.", source); 331 } 332 333 return (validationResult == null || validationResult.isPassing() ? ExitCode.OK : ExitCode.FAIL).exit(); 334 } 335 } 336}