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}