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; 030import gov.nist.secauto.oscal.tools.cli.core.utils.PrettyPrinter; 031 032import org.apache.commons.cli.CommandLine; 033import org.apache.commons.cli.Option; 034 035import java.io.IOException; 036import java.io.PrintStream; 037import java.net.URI; 038import java.net.URISyntaxException; 039import java.nio.file.Path; 040import java.util.Collection; 041import java.util.List; 042 043import edu.umd.cs.findbugs.annotations.NonNull; 044 045/** 046 * A command implementation supporting the resolution of an OSCAL profile. 047 */ 048public abstract class AbstractResolveCommand 049 extends AbstractTerminalCommand { 050 @NonNull 051 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 052 ExtraArgument.newInstance("URI to resolve", true), 053 ExtraArgument.newInstance("destination file", false))); 054 private static final Option RELATIVE_TO = Option.builder() 055 .longOpt("relative-to") 056 .desc("Generate URI references relative to this resource") 057 .hasArg() 058 .build(); 059 private static final Option PRETTY_PRINT_OPTION = Option.builder() 060 .longOpt("pretty-print") 061 .desc("Enable pretty-printing of the output for better readability") 062 .build(); 063 064 @NonNull 065 private static final List<Option> OPTIONS = ObjectUtils.notNull( 066 List.of( 067 MetaschemaCommands.AS_FORMAT_OPTION, 068 MetaschemaCommands.TO_OPTION, 069 MetaschemaCommands.OVERWRITE_OPTION, 070 RELATIVE_TO, 071 PRETTY_PRINT_OPTION)); 072 073 @Override 074 public String getDescription() { 075 return "Resolve the specified OSCAL Profile"; 076 } 077 078 @Override 079 public Collection<? extends Option> gatherOptions() { 080 return OPTIONS; 081 } 082 083 @Override 084 public List<ExtraArgument> getExtraArguments() { 085 return EXTRA_ARGUMENTS; 086 } 087 088 @SuppressWarnings({ 089 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable 090 "PMD.PreserveStackTrace" // intended 091 }) 092 093 @Override 094 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 095 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 096 } 097 098 /** 099 * 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}