1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.oscal.tools.cli.core.commands;
7   
8   import gov.nist.secauto.metaschema.cli.commands.MetaschemaCommands;
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.AbstractTerminalCommand;
12  import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
13  import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
14  import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
15  import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
16  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
17  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
18  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19  import gov.nist.secauto.metaschema.core.util.UriUtils;
20  import gov.nist.secauto.metaschema.databind.IBindingContext;
21  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
22  import gov.nist.secauto.metaschema.databind.io.Format;
23  import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
24  import gov.nist.secauto.metaschema.databind.io.ISerializer;
25  import gov.nist.secauto.oscal.lib.OscalBindingContext;
26  import gov.nist.secauto.oscal.lib.model.Catalog;
27  import gov.nist.secauto.oscal.lib.model.Profile;
28  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException;
29  import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
30  import gov.nist.secauto.oscal.tools.cli.core.utils.PrettyPrinter;
31  
32  import org.apache.commons.cli.CommandLine;
33  import org.apache.commons.cli.Option;
34  
35  import java.io.IOException;
36  import java.io.PrintStream;
37  import java.net.URI;
38  import java.net.URISyntaxException;
39  import java.nio.file.Path;
40  import java.util.Collection;
41  import java.util.List;
42  
43  import edu.umd.cs.findbugs.annotations.NonNull;
44  
45  /**
46   * A command implementation supporting the resolution of an OSCAL profile.
47   */
48  public abstract class AbstractResolveCommand
49      extends AbstractTerminalCommand {
50    @NonNull
51    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
52        ExtraArgument.newInstance("URI to resolve", true),
53        ExtraArgument.newInstance("destination file", false)));
54    private static final Option RELATIVE_TO = Option.builder()
55        .longOpt("relative-to")
56        .desc("Generate URI references relative to this resource")
57        .hasArg()
58        .build();
59    private static final Option PRETTY_PRINT_OPTION = Option.builder()
60        .longOpt("pretty-print")
61        .desc("Enable pretty-printing of the output for better readability")
62        .build();
63  
64    @NonNull
65    private static final List<Option> OPTIONS = ObjectUtils.notNull(
66        List.of(
67            MetaschemaCommands.AS_FORMAT_OPTION,
68            MetaschemaCommands.TO_OPTION,
69            MetaschemaCommands.OVERWRITE_OPTION,
70            RELATIVE_TO,
71            PRETTY_PRINT_OPTION));
72  
73    @Override
74    public String getDescription() {
75      return "Resolve the specified OSCAL Profile";
76    }
77  
78    @Override
79    public Collection<? extends Option> gatherOptions() {
80      return OPTIONS;
81    }
82  
83    @Override
84    public List<ExtraArgument> getExtraArguments() {
85      return EXTRA_ARGUMENTS;
86    }
87  
88    @SuppressWarnings({
89        "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable
90        "PMD.PreserveStackTrace" // intended
91    })
92  
93    @Override
94    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
95      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
96    }
97  
98    /**
99     * Process the command line arguments and execute the profile resolution
100    * operation.
101    *
102    * @param callingContext
103    *          the context information for the execution
104    * @param cmdLine
105    *          the parsed command line details
106    * @throws CommandExecutionException
107    *           if an error occurred while determining the source format
108    */
109   @SuppressWarnings({
110       "PMD.OnlyOneReturn", // readability
111       "PMD.CyclomaticComplexity"
112   })
113   protected void executeCommand(
114       @NonNull CallingContext callingContext,
115       @NonNull CommandLine cmdLine) throws CommandExecutionException {
116     List<String> extraArgs = cmdLine.getArgList();
117 
118     URI source = MetaschemaCommands.handleSource(
119         ObjectUtils.requireNonNull(extraArgs.get(0)),
120         ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
121 
122     IBindingContext bindingContext = OscalBindingContext.instance();
123     IBoundLoader loader = bindingContext.newBoundLoader();
124     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
125 
126     // attempt to determine the format
127     Format asFormat = MetaschemaCommands.determineSourceFormat(
128         cmdLine,
129         MetaschemaCommands.AS_FORMAT_OPTION,
130         loader,
131         source);
132 
133     IDocumentNodeItem document;
134     try {
135       document = loader.loadAsNodeItem(asFormat, source);
136     } catch (IOException ex) {
137       throw new CommandExecutionException(
138           ExitCode.IO_ERROR,
139           String.format("Unable to load content '%s'. %s",
140               source,
141               ex.getMessage()),
142           ex);
143     }
144 
145     Object object = document.getValue();
146     if (object == null) {
147       throw new CommandExecutionException(
148           ExitCode.INVALID_ARGUMENTS,
149           String.format("The source document '%s' contained no data.", source));
150     }
151 
152     if (object instanceof Catalog) {
153       // this is a catalog
154       throw new CommandExecutionException(
155           ExitCode.OK,
156           String.format("The source '%s' is already a catalog.", source));
157     }
158 
159     if (!(object instanceof Profile)) {
160       // this is something else
161       throw new CommandExecutionException(
162           ExitCode.INVALID_ARGUMENTS,
163           String.format("The source '%s' is not a profile.", source));
164     }
165 
166     Path destination = null;
167     if (extraArgs.size() > 1) {
168       destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
169     }
170 
171     URI relativeTo;
172     if (cmdLine.hasOption(RELATIVE_TO)) {
173       relativeTo = getCurrentWorkingDirectory().toUri().resolve(cmdLine.getOptionValue(RELATIVE_TO));
174     } else {
175       relativeTo = document.getDocumentUri();
176     }
177 
178     // this is a profile
179     DynamicContext dynamicContext = new DynamicContext(document.getStaticContext());
180     dynamicContext.setDocumentLoader(loader);
181     ProfileResolver resolver = new ProfileResolver(
182         dynamicContext,
183         (uri, src) -> {
184           try {
185             return UriUtils.relativize(relativeTo, src.resolve(uri), true);
186           } catch (URISyntaxException ex) {
187             throw new IllegalArgumentException(ex);
188           }
189         });
190 
191     IDocumentNodeItem resolvedProfile;
192     try {
193       resolvedProfile = resolver.resolve(document);
194     } catch (IOException | ProfileResolutionException ex) {
195       throw new CommandExecutionException(
196           ExitCode.PROCESSING_ERROR,
197           String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()),
198           ex);
199     }
200 
201     // DefaultConstraintValidator validator = new
202     // DefaultConstraintValidator(dynamicContext);
203     // ((IBoundXdmNodeItem)resolvedProfile).validate(validator);
204     // validator.finalizeValidation();
205 
206     Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
207     boolean prettyPrint = cmdLine.hasOption(PRETTY_PRINT_OPTION);
208     ISerializer<Catalog> serializer = bindingContext.newSerializer(toFormat, Catalog.class);
209     try {
210       if (destination == null) {
211         @SuppressWarnings({ "resource", "PMD.CloseResource" })
212         PrintStream stdOut = ObjectUtils.notNull(System.out);
213         serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut);
214       } else {
215         serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination);
216         if (prettyPrint) {
217           prettyPrintOutput(destination, toFormat);
218         }
219       }
220     } catch (IOException ex) {
221       throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
222     }
223   }
224 
225   /**
226    * Pretty-print the output file based on the specified format.
227    * <p>
228    * This feature was originally contributed by Mahesh Kumar Gaddam (ermahesh) in
229    * <a href="https://github.com/usnistgov/oscal-cli/pull/295">PR #295</a>.
230    * </p>
231    *
232    * @param destination
233    *          the path to the output file
234    * @param toFormat
235    *          the format of the output file
236    * @throws CommandExecutionException
237    *           if pretty-printing fails
238    */
239   @SuppressWarnings("PMD.PreserveStackTrace")
240   private void prettyPrintOutput(@NonNull Path destination, @NonNull Format toFormat)
241       throws CommandExecutionException {
242     try {
243       switch (toFormat) {
244       case JSON:
245         PrettyPrinter.prettyPrintJson(destination.toFile());
246         break;
247       case YAML:
248         PrettyPrinter.prettyPrintYaml(destination.toFile());
249         break;
250       case XML:
251         PrettyPrinter.prettyPrintXml(destination.toFile());
252         break;
253       default:
254         // do nothing for unknown formats
255         break;
256       }
257     } catch (Exception ex) {
258       throw new CommandExecutionException(
259           ExitCode.PROCESSING_ERROR,
260           String.format("Pretty-printing failed: %s", ex.getMessage()),
261           ex);
262     }
263   }
264 }