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.DefaultExtraArgument;
15  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
16  import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
17  import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
18  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
19  import gov.nist.secauto.metaschema.core.metapath.MetapathException;
20  import gov.nist.secauto.metaschema.core.model.IModule;
21  import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
22  import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
23  import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
24  import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
25  import gov.nist.secauto.metaschema.core.util.IVersionInfo;
26  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
27  import gov.nist.secauto.metaschema.databind.IBindingContext;
28  import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider;
29  import gov.nist.secauto.metaschema.databind.io.Format;
30  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
31  import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler;
32  
33  import org.apache.commons.cli.CommandLine;
34  import org.apache.commons.cli.Option;
35  import org.apache.logging.log4j.LogManager;
36  import org.apache.logging.log4j.Logger;
37  
38  import java.io.FileNotFoundException;
39  import java.io.IOException;
40  import java.net.URI;
41  import java.net.UnknownHostException;
42  import java.nio.file.Path;
43  import java.nio.file.Paths;
44  import java.util.Collection;
45  import java.util.List;
46  import java.util.Set;
47  
48  import edu.umd.cs.findbugs.annotations.NonNull;
49  import edu.umd.cs.findbugs.annotations.Nullable;
50  
51  /**
52   * Used by implementing classes to provide a content validation command.
53   */
54  public abstract class AbstractValidateContentCommand
55      extends AbstractTerminalCommand {
56    private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
57    @NonNull
58    private static final String COMMAND = "validate";
59    @NonNull
60    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
61        new DefaultExtraArgument("file-or-URI-to-validate", true)));
62  
63    @NonNull
64    private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
65        Option.builder("c")
66            .hasArgs()
67            .argName("URL")
68            .desc("additional constraint definitions")
69            .build());
70    @NonNull
71    private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull(
72        Option.builder("o")
73            .hasArg()
74            .argName("FILE")
75            .desc("write SARIF results to the provided FILE")
76            .numberOfArgs(1)
77            .build());
78    @NonNull
79    private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull(
80        Option.builder()
81            .longOpt("sarif-include-pass")
82            .desc("include pass results in SARIF")
83            .build());
84    @NonNull
85    private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull(
86        Option.builder()
87            .longOpt("disable-schema-validation")
88            .desc("do not perform schema validation")
89            .build());
90    @NonNull
91    private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull(
92        Option.builder()
93            .longOpt("disable-constraint-validation")
94            .desc("do not perform constraint validation")
95            .build());
96  
97    @Override
98    public String getName() {
99      return COMMAND;
100   }
101 
102   @SuppressWarnings("null")
103   @Override
104   public Collection<? extends Option> gatherOptions() {
105     return List.of(
106         MetaschemaCommands.AS_FORMAT_OPTION,
107         CONSTRAINTS_OPTION,
108         SARIF_OUTPUT_FILE_OPTION,
109         SARIF_INCLUDE_PASS_OPTION,
110         NO_SCHEMA_VALIDATION_OPTION,
111         NO_CONSTRAINT_VALIDATION_OPTION);
112   }
113 
114   @Override
115   public List<ExtraArgument> getExtraArguments() {
116     return EXTRA_ARGUMENTS;
117   }
118 
119   /**
120    * Drives the validation execution.
121    */
122   protected abstract class AbstractValidationCommandExecutor
123       extends AbstractCommandExecutor {
124 
125     /**
126      * Construct a new command executor.
127      *
128      * @param callingContext
129      *          the context of the command execution
130      * @param commandLine
131      *          the parsed command line details
132      */
133     public AbstractValidationCommandExecutor(
134         @NonNull CallingContext callingContext,
135         @NonNull CommandLine commandLine) {
136       super(callingContext, commandLine);
137     }
138 
139     /**
140      * Get the binding context to use for data processing.
141      *
142      * @param constraintSets
143      *          the constraints to configure in the resulting binding context
144      * @return the context
145      * @throws CommandExecutionException
146      *           if a error occurred while getting the binding context
147      */
148     @NonNull
149     protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets)
150         throws CommandExecutionException;
151 
152     /**
153      * Get the module to use for validation.
154      * <p>
155      * This module is used to generate schemas and as a source of built-in
156      * constraints.
157      *
158      * @param commandLine
159      *          the provided command line argument information
160      * @param bindingContext
161      *          the context used to access Metaschema module information based on
162      *          Java class bindings
163      * @return the loaded Metaschema module
164      * @throws CommandExecutionException
165      *           if an error occurred while loading the module
166      */
167     @NonNull
168     protected abstract IModule getModule(
169         @NonNull CommandLine commandLine,
170         @NonNull IBindingContext bindingContext)
171         throws CommandExecutionException;
172 
173     /**
174      * Get the schema validation implementation requested based on the provided
175      * command line arguments.
176      * <p>
177      * It is typical for this call to result in the dynamic generation of a schema
178      * to use for validation.
179      *
180      * @param module
181      *          the Metaschema module to generate the schema from
182      * @param commandLine
183      *          the provided command line argument information
184      * @param bindingContext
185      *          the context used to access Metaschema module information based on
186      *          Java class bindings
187      * @return the provider
188      */
189     @NonNull
190     protected abstract ISchemaValidationProvider getSchemaValidationProvider(
191         @NonNull IModule module,
192         @NonNull CommandLine commandLine,
193         @NonNull IBindingContext bindingContext);
194 
195     /**
196      * Execute the validation operation.
197      */
198     @SuppressWarnings("PMD.OnlyOneReturn") // readability
199     @Override
200     public void execute() throws CommandExecutionException {
201       CommandLine cmdLine = getCommandLine();
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         IModule module = bindingContext.registerModule(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           IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration);
268           validationResult = validationResult == null
269               ? constraintValidationResult
270               : AggregateValidationResult.aggregate(validationResult, constraintValidationResult);
271         }
272       } catch (FileNotFoundException ex) {
273         throw new CommandExecutionException(
274             ExitCode.IO_ERROR,
275             String.format("Resource not found at '%s'", source),
276             ex);
277       } catch (UnknownHostException ex) {
278         throw new CommandExecutionException(
279             ExitCode.IO_ERROR,
280             String.format("Unknown host for '%s'.", source),
281             ex);
282       } catch (IOException ex) {
283         throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
284       } catch (MetapathException ex) {
285         throw new CommandExecutionException(ExitCode.PROCESSING_ERROR, ex.getLocalizedMessage(), ex);
286       }
287       return validationResult;
288     }
289 
290     private void handleOutput(
291         @NonNull URI source,
292         @Nullable IValidationResult validationResult,
293         @NonNull CommandLine commandLine,
294         @NonNull IBindingContext bindingContext) throws CommandExecutionException {
295       if (commandLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) {
296         Path sarifFile = ObjectUtils.notNull(Paths.get(commandLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION)));
297 
298         IVersionInfo version
299             = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION);
300 
301         try {
302           SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version);
303           if (validationResult != null) {
304             sarifHandler.addFindings(validationResult.getFindings());
305           }
306           sarifHandler.write(sarifFile, bindingContext);
307         } catch (IOException ex) {
308           throw new CommandExecutionException(ExitCode.IO_ERROR, ex.getLocalizedMessage(), ex);
309         }
310       } else if (validationResult != null && !validationResult.getFindings().isEmpty()) {
311         LOGGER.info("Validation identified the following issues:");
312         LoggingValidationHandler.instance().handleResults(validationResult);
313       }
314 
315     }
316   }
317 }