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  
31  import org.apache.commons.cli.CommandLine;
32  import org.apache.commons.cli.Option;
33  
34  import java.io.IOException;
35  import java.io.PrintStream;
36  import java.net.URI;
37  import java.net.URISyntaxException;
38  import java.nio.file.Path;
39  import java.util.Collection;
40  import java.util.List;
41  
42  import edu.umd.cs.findbugs.annotations.NonNull;
43  
44  /**
45   * A command implementation supporting the resolution of an OSCAL profile.
46   */
47  public abstract class AbstractResolveCommand
48      extends AbstractTerminalCommand {
49    @NonNull
50    private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
51        ExtraArgument.newInstance("URI to resolve", true),
52        ExtraArgument.newInstance("destination file", false)));
53    private static final Option RELATIVE_TO = Option.builder()
54        .longOpt("relative-to")
55        .desc("Generate URI references relative to this resource")
56        .hasArg()
57        .build();
58  
59    @NonNull
60    private static final List<Option> OPTIONS = ObjectUtils.notNull(
61        List.of(
62            MetaschemaCommands.AS_FORMAT_OPTION,
63            MetaschemaCommands.TO_OPTION,
64            MetaschemaCommands.OVERWRITE_OPTION,
65            RELATIVE_TO));
66  
67    @Override
68    public String getDescription() {
69      return "Resolve the specified OSCAL Profile";
70    }
71  
72    @Override
73    public Collection<? extends Option> gatherOptions() {
74      return OPTIONS;
75    }
76  
77    @Override
78    public List<ExtraArgument> getExtraArguments() {
79      return EXTRA_ARGUMENTS;
80    }
81  
82    @SuppressWarnings({
83        "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable
84        "PMD.PreserveStackTrace" // intended
85    })
86  
87    @Override
88    public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
89      return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
90    }
91  
92    /**
93     * Process the command line arguments and execute the profile resolution
94     * operation.
95     *
96     * @param callingContext
97     *          the context information for the execution
98     * @param cmdLine
99     *          the parsed command line details
100    * @throws CommandExecutionException
101    *           if an error occurred while determining the source format
102    */
103   @SuppressWarnings({
104       "PMD.OnlyOneReturn", // readability
105       "PMD.CyclomaticComplexity"
106   })
107   protected void executeCommand(
108       @NonNull CallingContext callingContext,
109       @NonNull CommandLine cmdLine) throws CommandExecutionException {
110     List<String> extraArgs = cmdLine.getArgList();
111 
112     URI source = MetaschemaCommands.handleSource(
113         ObjectUtils.requireNonNull(extraArgs.get(0)),
114         ObjectUtils.notNull(getCurrentWorkingDirectory().toUri()));
115 
116     IBindingContext bindingContext = OscalBindingContext.instance();
117     IBoundLoader loader = bindingContext.newBoundLoader();
118     loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
119 
120     // attempt to determine the format
121     Format asFormat = MetaschemaCommands.determineSourceFormat(
122         cmdLine,
123         MetaschemaCommands.AS_FORMAT_OPTION,
124         loader,
125         source);
126 
127     IDocumentNodeItem document;
128     try {
129       document = loader.loadAsNodeItem(asFormat, source);
130     } catch (IOException ex) {
131       throw new CommandExecutionException(
132           ExitCode.IO_ERROR,
133           String.format("Unable to load content '%s'. %s",
134               source,
135               ex.getMessage()),
136           ex);
137     }
138 
139     Object object = document.getValue();
140     if (object == null) {
141       throw new CommandExecutionException(
142           ExitCode.INVALID_ARGUMENTS,
143           String.format("The source document '%s' contained no data.", source));
144     }
145 
146     if (object instanceof Catalog) {
147       // this is a catalog
148       throw new CommandExecutionException(
149           ExitCode.OK,
150           String.format("The source '%s' is already a catalog.", source));
151     }
152 
153     if (!(object instanceof Profile)) {
154       // this is something else
155       throw new CommandExecutionException(
156           ExitCode.INVALID_ARGUMENTS,
157           String.format("The source '%s' is not a profile.", source));
158     }
159 
160     Path destination = null;
161     if (extraArgs.size() > 1) {
162       destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine);
163     }
164 
165     URI relativeTo;
166     if (cmdLine.hasOption(RELATIVE_TO)) {
167       relativeTo = getCurrentWorkingDirectory().toUri().resolve(cmdLine.getOptionValue(RELATIVE_TO));
168     } else {
169       relativeTo = document.getDocumentUri();
170     }
171 
172     // this is a profile
173     DynamicContext dynamicContext = new DynamicContext(document.getStaticContext());
174     dynamicContext.setDocumentLoader(loader);
175     ProfileResolver resolver = new ProfileResolver(
176         dynamicContext,
177         (uri, src) -> {
178           try {
179             return UriUtils.relativize(relativeTo, src.resolve(uri), true);
180           } catch (URISyntaxException ex) {
181             throw new IllegalArgumentException(ex);
182           }
183         });
184 
185     IDocumentNodeItem resolvedProfile;
186     try {
187       resolvedProfile = resolver.resolve(document);
188     } catch (IOException | ProfileResolutionException ex) {
189       throw new CommandExecutionException(
190           ExitCode.PROCESSING_ERROR,
191           String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()),
192           ex);
193     }
194 
195     // DefaultConstraintValidator validator = new
196     // DefaultConstraintValidator(dynamicContext);
197     // ((IBoundXdmNodeItem)resolvedProfile).validate(validator);
198     // validator.finalizeValidation();
199 
200     Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION);
201     ISerializer<Catalog> serializer = bindingContext.newSerializer(toFormat, Catalog.class);
202     try {
203       if (destination == null) {
204         @SuppressWarnings({ "resource", "PMD.CloseResource" })
205         PrintStream stdOut = ObjectUtils.notNull(System.out);
206         serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut);
207       } else {
208         serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination);
209       }
210     } catch (IOException ex) {
211       throw new CommandExecutionException(ExitCode.IO_ERROR, ex);
212     }
213   }
214 }