001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.oscal.tools.cli.core.commands;
007
008import gov.nist.secauto.metaschema.cli.commands.MetaschemaCommands;
009import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
010import gov.nist.secauto.metaschema.cli.processor.ExitCode;
011import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
012import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException;
013import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
014import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
015import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
016import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
017import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
018import gov.nist.secauto.metaschema.core.util.ObjectUtils;
019import gov.nist.secauto.metaschema.core.util.UriUtils;
020import gov.nist.secauto.metaschema.databind.IBindingContext;
021import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
022import gov.nist.secauto.metaschema.databind.io.Format;
023import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
024import gov.nist.secauto.metaschema.databind.io.ISerializer;
025import gov.nist.secauto.oscal.lib.OscalBindingContext;
026import gov.nist.secauto.oscal.lib.model.Catalog;
027import gov.nist.secauto.oscal.lib.model.Profile;
028import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException;
029import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
030
031import org.apache.commons.cli.CommandLine;
032import org.apache.commons.cli.Option;
033
034import java.io.IOException;
035import java.io.PrintStream;
036import java.net.URI;
037import java.net.URISyntaxException;
038import java.nio.file.Path;
039import java.util.Collection;
040import java.util.List;
041
042import edu.umd.cs.findbugs.annotations.NonNull;
043
044/**
045 * A command implementation supporting the resolution of an OSCAL profile.
046 */
047public abstract class AbstractResolveCommand
048    extends AbstractTerminalCommand {
049  @NonNull
050  private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
051      ExtraArgument.newInstance("URI to resolve", true),
052      ExtraArgument.newInstance("destination file", false)));
053  private static final Option RELATIVE_TO = Option.builder()
054      .longOpt("relative-to")
055      .desc("Generate URI references relative to this resource")
056      .hasArg()
057      .build();
058
059  @NonNull
060  private static final List<Option> OPTIONS = ObjectUtils.notNull(
061      List.of(
062          MetaschemaCommands.AS_FORMAT_OPTION,
063          MetaschemaCommands.TO_OPTION,
064          MetaschemaCommands.OVERWRITE_OPTION,
065          RELATIVE_TO));
066
067  @Override
068  public String getDescription() {
069    return "Resolve the specified OSCAL Profile";
070  }
071
072  @Override
073  public Collection<? extends Option> gatherOptions() {
074    return OPTIONS;
075  }
076
077  @Override
078  public List<ExtraArgument> getExtraArguments() {
079    return EXTRA_ARGUMENTS;
080  }
081
082  @SuppressWarnings({
083      "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable
084      "PMD.PreserveStackTrace" // intended
085  })
086
087  @Override
088  public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
089    return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
090  }
091
092  /**
093   * Process the command line arguments and execute the profile resolution
094   * operation.
095   *
096   * @param callingContext
097   *          the context information for the execution
098   * @param cmdLine
099   *          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}