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       URI currentWorkingDirectory = ObjectUtils.notNull(getCurrentWorkingDirectory().toUri());
202 
203       Set<IConstraintSet> constraintSets = MetaschemaCommands.loadConstraintSets(
204           cmdLine,
205           CONSTRAINTS_OPTION,
206           currentWorkingDirectory);
207 
208       List<String> extraArgs = cmdLine.getArgList();
209 
210       URI source = MetaschemaCommands.handleSource(
211           ObjectUtils.requireNonNull(extraArgs.get(0)),
212           currentWorkingDirectory);
213 
214       IBindingContext bindingContext = getBindingContext(constraintSets);
215       IBoundLoader loader = bindingContext.newBoundLoader();
216       Format asFormat = MetaschemaCommands.determineSourceFormat(
217           cmdLine,
218           MetaschemaCommands.AS_FORMAT_OPTION,
219           loader,
220           source);
221 
222       IValidationResult validationResult = validate(source, asFormat, cmdLine, bindingContext);
223       handleOutput(source, validationResult, cmdLine, bindingContext);
224 
225       if (validationResult == null || validationResult.isPassing()) {
226         if (LOGGER.isInfoEnabled()) {
227           LOGGER.info("The file '{}' is valid.", source);
228         }
229       } else if (LOGGER.isErrorEnabled()) {
230         LOGGER.error("The file '{}' is invalid.", source);
231       }
232 
233       if (validationResult != null && !validationResult.isPassing()) {
234         throw new CommandExecutionException(ExitCode.FAIL);
235       }
236     }
237 
238     @SuppressWarnings("PMD.CyclomaticComplexity")
239     @Nullable
240     private IValidationResult validate(
241         @NonNull URI source,
242         @NonNull Format asFormat,
243         @NonNull CommandLine commandLine,
244         @NonNull IBindingContext bindingContext) throws CommandExecutionException {
245 
246       if (LOGGER.isInfoEnabled()) {
247         LOGGER.info("Validating '{}' as {}.", source, asFormat.name());
248       }
249 
250       IValidationResult validationResult = null;
251       try {
252         // get the module, but don't register it
253         IModule module = getModule(commandLine, bindingContext);
254         if (!commandLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) {
255           // perform schema validation
256           validationResult = getSchemaValidationProvider(module, commandLine, bindingContext)
257               .validateWithSchema(source, asFormat, bindingContext);
258         }
259 
260         if (!commandLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION)) {
261           IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>();
262           if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && commandLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) {
263             configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS);
264           }
265 
266           // perform constraint validation
267           bindingContext.registerModule(module); // ensure the module is registered
268           IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration);
269           validationResult = validationResult == null
270               ? constraintValidationResult
271               : AggregateValidationResult.aggregate(validationResult, constraintValidationResult);
272         }
273       } catch (FileNotFoundException ex) {
274         throw new CommandExecutionException(
275             ExitCode.IO_ERROR,
276             String.format("Resource not found at '%s'", source),
277             ex);
278       } catch (UnknownHostException ex) {
279         throw new CommandExecutionException(
280             ExitCode.IO_ERROR,
281             String.format("Unknown host for '%s'.", source),
282             ex);
283       } catch (IOException ex) {
284         throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
285       } catch (MetapathException ex) {
286         throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex.getLocalizedMessage(), ex);
287       }
288       return validationResult;
289     }
290 
291     private void handleOutput(
292         @NonNull URI source,
293         @Nullable IValidationResult validationResult,
294         @NonNull CommandLine commandLine,
295         @NonNull IBindingContext bindingContext) throws CommandExecutionException {
296       if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) {
297         Path sarifFile = ObjectUtils.notNull(Paths.get(commandLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION)));
298 
299         IVersionInfo version
300             = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION);
301 
302         try {
303           SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version);
304           if (validationResult != null) {
305             sarifHandler.addFindings(validationResult.getFindings());
306           }
307           sarifHandler.write(sarifFile, bindingContext);
308         } catch (IOException ex) {
309           throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
310         }
311       } else if (validationResult != null && !validationResult.getFindings().isEmpty()) {
312         LOGGER.info("Validation identified the following issues:");
313         LoggingValidationHandler.instance().handleResults(validationResult);
314       }
315 
316     }
317   }
318 }