1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli.commands;
7   
8   import gov.nist.secauto.metaschema.cli.processor.CLIProcessor;
9   import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
10  import gov.nist.secauto.metaschema.cli.processor.ExitCode;
11  import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
12  import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
13  import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
14  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
15  import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
16  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
17  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
18  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
19  import gov.nist.secauto.metaschema.core.model.IModule;
20  import gov.nist.secauto.metaschema.core.model.MetaschemaException;
21  import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationException;
22  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
23  import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
24  import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
25  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
26  import gov.nist.secauto.metaschema.core.util.IVersionInfo;
27  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
28  import gov.nist.secauto.metaschema.databind.IBindingContext;
29  import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider;
30  import gov.nist.secauto.metaschema.databind.io.Format;
31  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
32  import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler;
33  
34  import org.apache.commons.cli.CommandLine;
35  import org.apache.commons.cli.Option;
36  import org.apache.logging.log4j.LogManager;
37  import org.apache.logging.log4j.Logger;
38  
39  import java.io.FileNotFoundException;
40  import java.io.IOException;
41  import java.net.URI;
42  import java.net.UnknownHostException;
43  import java.nio.file.Path;
44  import java.nio.file.Paths;
45  import java.util.Collection;
46  import java.util.List;
47  import java.util.Set;
48  
49  import edu.umd.cs.findbugs.annotations.NonNull;
50  import edu.umd.cs.findbugs.annotations.Nullable;
51  
52  /**
53   * Used by implementing classes to provide a content validation command.
54   */
55  public abstract class AbstractValidateContentCommand
56      extends AbstractTerminalCommand {
57    private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
58    @NonNull
59    private static final String COMMAND = "validate";
60    @NonNull
61    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
62        ExtraArgument.newInstance("file-or-URI-to-validate", true)));
63  
64    @NonNull
65    private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
66        Option.builder("c")
67            .hasArgs()
68            .argName("URL")
69            .desc("additional constraint definitions")
70            .get());
71    @NonNull
72    private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull(
73        Option.builder("o")
74            .hasArg()
75            .argName("FILE")
76            .desc("write SARIF results to the provided FILE")
77            .numberOfArgs(1)
78            .get());
79    @NonNull
80    private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull(
81        Option.builder()
82            .longOpt("sarif-include-pass")
83            .desc("include pass results in SARIF")
84            .get());
85    @NonNull
86    private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull(
87        Option.builder()
88            .longOpt("disable-schema-validation")
89            .desc("do not perform schema validation")
90            .get());
91    @NonNull
92    private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull(
93        Option.builder()
94            .longOpt("disable-constraint-validation")
95            .desc("do not perform constraint validation")
96            .get());
97  
98    @Override
99    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 }