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}