1
2
3
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.ExitStatus;
12 import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
13 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
14 import gov.nist.secauto.metaschema.cli.processor.command.AbstractCommandExecutor;
15 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
16 import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
17 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
18 import gov.nist.secauto.metaschema.cli.util.LoggingValidationHandler;
19 import gov.nist.secauto.metaschema.core.configuration.DefaultConfiguration;
20 import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
21 import gov.nist.secauto.metaschema.core.metapath.MetapathException;
22 import gov.nist.secauto.metaschema.core.model.IConstraintLoader;
23 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
24 import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
25 import gov.nist.secauto.metaschema.core.model.constraint.ValidationFeature;
26 import gov.nist.secauto.metaschema.core.model.validation.AggregateValidationResult;
27 import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
28 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
29 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
30 import gov.nist.secauto.metaschema.core.util.IVersionInfo;
31 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
32 import gov.nist.secauto.metaschema.core.util.UriUtils;
33 import gov.nist.secauto.metaschema.databind.IBindingContext;
34 import gov.nist.secauto.metaschema.databind.IBindingContext.ISchemaValidationProvider;
35 import gov.nist.secauto.metaschema.databind.io.Format;
36 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
37 import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader;
38 import gov.nist.secauto.metaschema.modules.sarif.SarifValidationHandler;
39
40 import org.apache.commons.cli.CommandLine;
41 import org.apache.commons.cli.Option;
42 import org.apache.logging.log4j.LogManager;
43 import org.apache.logging.log4j.Logger;
44
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.net.URI;
48 import java.net.URISyntaxException;
49 import java.net.UnknownHostException;
50 import java.nio.file.Path;
51 import java.nio.file.Paths;
52 import java.util.Arrays;
53 import java.util.Collection;
54 import java.util.LinkedHashSet;
55 import java.util.List;
56 import java.util.Locale;
57 import java.util.Set;
58
59 import edu.umd.cs.findbugs.annotations.NonNull;
60
61 public abstract class AbstractValidateContentCommand
62 extends AbstractTerminalCommand {
63 private static final Logger LOGGER = LogManager.getLogger(AbstractValidateContentCommand.class);
64 @NonNull
65 private static final String COMMAND = "validate";
66 @NonNull
67 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
68 new DefaultExtraArgument("file-or-URI-to-validate", true)));
69
70 @NonNull
71 private static final Option AS_OPTION = ObjectUtils.notNull(
72 Option.builder()
73 .longOpt("as")
74 .hasArg()
75 .argName("FORMAT")
76 .desc("source format: xml, json, or yaml")
77 .numberOfArgs(1)
78 .build());
79 @NonNull
80 private static final Option CONSTRAINTS_OPTION = ObjectUtils.notNull(
81 Option.builder("c")
82 .hasArgs()
83 .argName("URL")
84 .desc("additional constraint definitions")
85 .build());
86 @NonNull
87 private static final Option SARIF_OUTPUT_FILE_OPTION = ObjectUtils.notNull(
88 Option.builder("o")
89 .hasArg()
90 .argName("FILE")
91 .desc("write SARIF results to the provided FILE")
92 .numberOfArgs(1)
93 .build());
94 @NonNull
95 private static final Option SARIF_INCLUDE_PASS_OPTION = ObjectUtils.notNull(
96 Option.builder()
97 .longOpt("sarif-include-pass")
98 .desc("include pass results in SARIF")
99 .build());
100 @NonNull
101 private static final Option NO_SCHEMA_VALIDATION_OPTION = ObjectUtils.notNull(
102 Option.builder()
103 .longOpt("disable-schema-validation")
104 .desc("do not perform schema validation")
105 .build());
106 @NonNull
107 private static final Option NO_CONSTRAINT_VALIDATION_OPTION = ObjectUtils.notNull(
108 Option.builder()
109 .longOpt("disable-constraint-validation")
110 .desc("do not perform constraint validation")
111 .build());
112
113 @Override
114 public String getName() {
115 return COMMAND;
116 }
117
118 @SuppressWarnings("null")
119 @Override
120 public Collection<? extends Option> gatherOptions() {
121 return List.of(
122 AS_OPTION,
123 CONSTRAINTS_OPTION,
124 SARIF_OUTPUT_FILE_OPTION,
125 SARIF_INCLUDE_PASS_OPTION,
126 NO_SCHEMA_VALIDATION_OPTION,
127 NO_CONSTRAINT_VALIDATION_OPTION);
128 }
129
130 @Override
131 public List<ExtraArgument> getExtraArguments() {
132 return EXTRA_ARGUMENTS;
133 }
134
135 @SuppressWarnings("PMD.PreserveStackTrace")
136 @Override
137 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
138 List<String> extraArgs = cmdLine.getArgList();
139 if (extraArgs.size() != 1) {
140 throw new InvalidArgumentException("The source to validate must be provided.");
141 }
142
143 if (cmdLine.hasOption(AS_OPTION)) {
144 try {
145 String toFormatText = cmdLine.getOptionValue(AS_OPTION);
146 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
147 } catch (IllegalArgumentException ex) {
148 InvalidArgumentException newEx = new InvalidArgumentException(
149 String.format("Invalid '%s' argument. The format must be one of: %s.",
150 OptionUtils.toArgument(AS_OPTION),
151 Arrays.asList(Format.values()).stream()
152 .map(format -> format.name())
153 .collect(CustomCollectors.joiningWithOxfordComma("and"))));
154 newEx.addSuppressed(ex);
155 throw newEx;
156 }
157 }
158 }
159
160 protected abstract class AbstractValidationCommandExecutor
161 extends AbstractCommandExecutor
162 implements ISchemaValidationProvider {
163
164
165
166
167
168
169
170
171
172 public AbstractValidationCommandExecutor(
173 @NonNull CallingContext callingContext,
174 @NonNull CommandLine commandLine) {
175 super(callingContext, commandLine);
176 }
177
178
179
180
181
182
183
184
185
186
187
188
189 @NonNull
190 protected abstract IBindingContext getBindingContext(@NonNull Set<IConstraintSet> constraintSets)
191 throws MetaschemaException, IOException;
192
193 @SuppressWarnings("PMD.OnlyOneReturn")
194 @Override
195 public ExitStatus execute() {
196 URI cwd = ObjectUtils.notNull(Paths.get("").toAbsolutePath().toUri());
197 CommandLine cmdLine = getCommandLine();
198
199 Set<IConstraintSet> constraintSets;
200 if (cmdLine.hasOption(CONSTRAINTS_OPTION)) {
201 IConstraintLoader constraintLoader = new BindingConstraintLoader(IBindingContext.instance());
202 constraintSets = new LinkedHashSet<>();
203 String[] args = cmdLine.getOptionValues(CONSTRAINTS_OPTION);
204 for (String arg : args) {
205 assert arg != null;
206 try {
207 URI constraintUri = ObjectUtils.requireNonNull(UriUtils.toUri(arg, cwd));
208 constraintSets.addAll(constraintLoader.load(constraintUri));
209 } catch (IOException | MetaschemaException | MetapathException | URISyntaxException ex) {
210 return ExitCode.IO_ERROR.exitMessage("Unable to load constraint set '" + arg + "'.").withThrowable(ex);
211 }
212 }
213 } else {
214 constraintSets = CollectionUtil.emptySet();
215 }
216
217 IBindingContext bindingContext;
218 try {
219 bindingContext = getBindingContext(constraintSets);
220 } catch (IOException | MetaschemaException ex) {
221 return ExitCode.PROCESSING_ERROR
222 .exitMessage("Unable to get binding context. " + ex.getMessage())
223 .withThrowable(ex);
224 }
225
226 IBoundLoader loader = bindingContext.newBoundLoader();
227
228 List<String> extraArgs = cmdLine.getArgList();
229
230 String sourceName = ObjectUtils.requireNonNull(extraArgs.get(0));
231 URI source;
232
233 try {
234 source = UriUtils.toUri(sourceName, cwd);
235 } catch (URISyntaxException ex) {
236 return ExitCode.IO_ERROR.exitMessage("Cannot load source '%s' as it is not a valid file or URI.")
237 .withThrowable(ex);
238 }
239
240 Format asFormat;
241 if (cmdLine.hasOption(AS_OPTION)) {
242 try {
243 String toFormatText = cmdLine.getOptionValue(AS_OPTION);
244 asFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
245 } catch (IllegalArgumentException ex) {
246 return ExitCode.IO_ERROR
247 .exitMessage("Invalid '--as' argument. The format must be one of: "
248 + Arrays.stream(Format.values())
249 .map(format -> format.name())
250 .collect(CustomCollectors.joiningWithOxfordComma("or")))
251 .withThrowable(ex);
252 }
253 } else {
254
255 try {
256 asFormat = loader.detectFormat(source);
257 } catch (FileNotFoundException ex) {
258
259 return ExitCode.IO_ERROR.exitMessage("The provided source file '" + source + "' does not exist.");
260 } catch (IOException ex) {
261 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
262 } catch (IllegalArgumentException ex) {
263 return ExitCode.IO_ERROR.exitMessage(
264 "Source file has unrecognizable format. Use '--as' to specify the format. The format must be one of: "
265 + Arrays.stream(Format.values())
266 .map(format -> format.name())
267 .collect(CustomCollectors.joiningWithOxfordComma("or")));
268 }
269 }
270
271 if (LOGGER.isInfoEnabled()) {
272 LOGGER.info("Validating '{}' as {}.", source, asFormat.name());
273 }
274
275 IMutableConfiguration<ValidationFeature<?>> configuration = new DefaultConfiguration<>();
276 if (cmdLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && cmdLine.hasOption(SARIF_INCLUDE_PASS_OPTION)) {
277 configuration.enableFeature(ValidationFeature.VALIDATE_GENERATE_PASS_FINDINGS);
278 }
279
280 IValidationResult validationResult = null;
281 try {
282 if (!cmdLine.hasOption(NO_SCHEMA_VALIDATION_OPTION)) {
283
284 validationResult = this.validateWithSchema(source, asFormat);
285 }
286
287 if (!cmdLine.hasOption(NO_CONSTRAINT_VALIDATION_OPTION)
288 && (validationResult == null || validationResult.isPassing())) {
289
290 IValidationResult constraintValidationResult = bindingContext.validateWithConstraints(source, configuration);
291 validationResult = validationResult == null
292 ? constraintValidationResult
293 : AggregateValidationResult.aggregate(validationResult, constraintValidationResult);
294 }
295 } catch (FileNotFoundException ex) {
296 return ExitCode.IO_ERROR.exitMessage(String.format("Resource not found at '%s'", source)).withThrowable(ex);
297 } catch (UnknownHostException ex) {
298 return ExitCode.IO_ERROR.exitMessage(String.format("Unknown host for '%s'.", source)).withThrowable(ex);
299 } catch (IOException ex) {
300 return ExitCode.IO_ERROR.exit().withThrowable(ex);
301 } catch (MetapathException ex) {
302 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
303 }
304
305 if (cmdLine.hasOption(SARIF_OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) {
306 Path sarifFile = ObjectUtils.notNull(Paths.get(cmdLine.getOptionValue(SARIF_OUTPUT_FILE_OPTION)));
307
308 IVersionInfo version
309 = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION);
310
311 try {
312 SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version);
313 if (validationResult != null) {
314 sarifHandler.addFindings(validationResult.getFindings());
315 }
316 sarifHandler.write(sarifFile);
317 } catch (IOException ex) {
318 return ExitCode.IO_ERROR.exit().withThrowable(ex);
319 }
320 } else if (validationResult != null && !validationResult.getFindings().isEmpty()) {
321 LOGGER.info("Validation identified the following issues:", source);
322 LoggingValidationHandler.instance().handleValidationResults(validationResult);
323 }
324
325 if (validationResult == null || validationResult.isPassing()) {
326 if (LOGGER.isInfoEnabled()) {
327 LOGGER.info("The file '{}' is valid.", source);
328 }
329 } else if (LOGGER.isErrorEnabled()) {
330 LOGGER.error("The file '{}' is invalid.", source);
331 }
332
333 return (validationResult == null || validationResult.isPassing() ? ExitCode.OK : ExitCode.FAIL).exit();
334 }
335 }
336 }