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.databind.IBindingContext; 020import gov.nist.secauto.metaschema.databind.io.DeserializationFeature; 021import gov.nist.secauto.metaschema.databind.io.Format; 022import gov.nist.secauto.metaschema.databind.io.IBoundLoader; 023import gov.nist.secauto.metaschema.databind.io.ISerializer; 024import gov.nist.secauto.oscal.lib.OscalBindingContext; 025import gov.nist.secauto.oscal.lib.model.Catalog; 026import gov.nist.secauto.oscal.lib.model.Profile; 027import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException; 028import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver; 029 030import org.apache.commons.cli.CommandLine; 031import org.apache.commons.cli.Option; 032 033import java.io.IOException; 034import java.io.PrintStream; 035import java.net.URI; 036import java.nio.file.Path; 037import java.util.Collection; 038import java.util.List; 039 040import edu.umd.cs.findbugs.annotations.NonNull; 041 042/** 043 * A command implementation supporting the resolution of an OSCAL profile. 044 */ 045public abstract class AbstractResolveCommand 046 extends AbstractTerminalCommand { 047 @NonNull 048 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of( 049 ExtraArgument.newInstance("URI to resolve", true), 050 ExtraArgument.newInstance("destination file", false))); 051 @NonNull 052 private static final List<Option> OPTIONS = ObjectUtils.notNull( 053 List.of( 054 MetaschemaCommands.AS_FORMAT_OPTION, 055 MetaschemaCommands.TO_OPTION, 056 MetaschemaCommands.OVERWRITE_OPTION)); 057 058 @Override 059 public String getDescription() { 060 return "Resolve the specified OSCAL Profile"; 061 } 062 063 @Override 064 public Collection<? extends Option> gatherOptions() { 065 return OPTIONS; 066 } 067 068 @Override 069 public List<ExtraArgument> getExtraArguments() { 070 return EXTRA_ARGUMENTS; 071 } 072 073 @SuppressWarnings({ 074 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity", // reasonable 075 "PMD.PreserveStackTrace" // intended 076 }) 077 078 @Override 079 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) { 080 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand); 081 } 082 083 /** 084 * Process the command line arguments and execute the profile resolution 085 * operation. 086 * 087 * @param callingContext 088 * the context information for the execution 089 * @param cmdLine 090 * the parsed command line details 091 * @throws CommandExecutionException 092 * if an error occurred while determining the source format 093 */ 094 @SuppressWarnings({ 095 "PMD.OnlyOneReturn", // readability 096 "PMD.CyclomaticComplexity" 097 }) 098 protected void executeCommand( 099 @NonNull CallingContext callingContext, 100 @NonNull CommandLine cmdLine) throws CommandExecutionException { 101 List<String> extraArgs = cmdLine.getArgList(); 102 103 URI source = MetaschemaCommands.handleSource( 104 ObjectUtils.requireNonNull(extraArgs.get(0)), 105 ObjectUtils.notNull(getCurrentWorkingDirectory().toUri())); 106 107 IBindingContext bindingContext = OscalBindingContext.instance(); 108 IBoundLoader loader = bindingContext.newBoundLoader(); 109 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS); 110 111 // attempt to determine the format 112 Format asFormat = MetaschemaCommands.determineSourceFormat( 113 cmdLine, 114 MetaschemaCommands.AS_FORMAT_OPTION, 115 loader, 116 source); 117 118 IDocumentNodeItem document; 119 try { 120 document = loader.loadAsNodeItem(asFormat, source); 121 } catch (IOException ex) { 122 throw new CommandExecutionException( 123 ExitCode.IO_ERROR, 124 String.format("Unable to load content '%s'. %s", 125 source, 126 ex.getMessage()), 127 ex); 128 } 129 130 Object object = document.getValue(); 131 if (object == null) { 132 throw new CommandExecutionException( 133 ExitCode.INVALID_ARGUMENTS, 134 String.format("The source document '%s' contained no data.", source)); 135 } 136 137 if (object instanceof Catalog) { 138 // this is a catalog 139 throw new CommandExecutionException( 140 ExitCode.OK, 141 String.format("The source '%s' is already a catalog.", source)); 142 } 143 144 if (!(object instanceof Profile)) { 145 // this is something else 146 throw new CommandExecutionException( 147 ExitCode.INVALID_ARGUMENTS, 148 String.format("The source '%s' is not a profile.", source)); 149 } 150 151 Path destination = null; 152 if (extraArgs.size() > 1) { 153 destination = MetaschemaCommands.handleDestination(ObjectUtils.requireNonNull(extraArgs.get(1)), cmdLine); 154 } 155 156 // this is a profile 157 DynamicContext dynamicContext = new DynamicContext(document.getStaticContext()); 158 dynamicContext.setDocumentLoader(loader); 159 ProfileResolver resolver = new ProfileResolver(dynamicContext); 160 161 IDocumentNodeItem resolvedProfile; 162 try { 163 resolvedProfile = resolver.resolve(document); 164 } catch (IOException | ProfileResolutionException ex) { 165 throw new CommandExecutionException( 166 ExitCode.PROCESSING_ERROR, 167 String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()), 168 ex); 169 } 170 171 // DefaultConstraintValidator validator = new 172 // DefaultConstraintValidator(dynamicContext); 173 // ((IBoundXdmNodeItem)resolvedProfile).validate(validator); 174 // validator.finalizeValidation(); 175 176 Format toFormat = MetaschemaCommands.getFormat(cmdLine, MetaschemaCommands.TO_OPTION); 177 ISerializer<Catalog> serializer = bindingContext.newSerializer(toFormat, Catalog.class); 178 try { 179 if (destination == null) { 180 @SuppressWarnings({ "resource", "PMD.CloseResource" }) 181 PrintStream stdOut = ObjectUtils.notNull(System.out); 182 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut); 183 } else { 184 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination); 185 } 186 } catch (IOException ex) { 187 throw new CommandExecutionException(ExitCode.IO_ERROR, ex); 188 } 189 } 190}