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}