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