1
2
3
4
5
6 package gov.nist.secauto.oscal.tools.cli.core.commands;
7
8 import gov.nist.secauto.metaschema.cli.processor.CLIProcessor.CallingContext;
9 import gov.nist.secauto.metaschema.cli.processor.ExitCode;
10 import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
11 import gov.nist.secauto.metaschema.cli.processor.InvalidArgumentException;
12 import gov.nist.secauto.metaschema.cli.processor.OptionUtils;
13 import gov.nist.secauto.metaschema.cli.processor.command.AbstractTerminalCommand;
14 import gov.nist.secauto.metaschema.cli.processor.command.DefaultExtraArgument;
15 import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument;
16 import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor;
17 import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
18 import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
19 import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem;
20 import gov.nist.secauto.metaschema.core.util.CustomCollectors;
21 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
22 import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
23 import gov.nist.secauto.metaschema.databind.io.Format;
24 import gov.nist.secauto.metaschema.databind.io.IBoundLoader;
25 import gov.nist.secauto.metaschema.databind.io.ISerializer;
26 import gov.nist.secauto.oscal.lib.OscalBindingContext;
27 import gov.nist.secauto.oscal.lib.model.Catalog;
28 import gov.nist.secauto.oscal.lib.model.Profile;
29 import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolutionException;
30 import gov.nist.secauto.oscal.lib.profile.resolver.ProfileResolver;
31
32 import org.apache.commons.cli.CommandLine;
33 import org.apache.commons.cli.Option;
34
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.IOException;
38 import java.io.PrintStream;
39 import java.nio.file.Files;
40 import java.nio.file.Path;
41 import java.nio.file.Paths;
42 import java.util.Arrays;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Locale;
46
47 import edu.umd.cs.findbugs.annotations.NonNull;
48
49 public abstract class AbstractResolveCommand
50 extends AbstractTerminalCommand {
51 @NonNull
52 private static final List<ExtraArgument> EXTRA_ARGUMENTS = ObjectUtils.notNull(List.of(
53 new DefaultExtraArgument("file to resolve", true),
54 new DefaultExtraArgument("destination file", false)));
55 @NonNull
56 private static final Option AS_OPTION = ObjectUtils.notNull(
57 Option.builder()
58 .longOpt("as")
59 .hasArg()
60 .argName("FORMAT")
61 .desc("source format: xml, json, or yaml")
62 .build());
63 @NonNull
64 private static final Option TO_OPTION = ObjectUtils.notNull(
65 Option.builder()
66 .longOpt("to")
67 .required()
68 .hasArg().argName("FORMAT")
69 .desc("convert to format: xml, json, or yaml")
70 .build());
71 @NonNull
72 private static final Option OVERWRITE_OPTION = ObjectUtils.notNull(
73 Option.builder()
74 .longOpt("overwrite")
75 .desc("overwrite the destination if it exists")
76 .build());
77 @NonNull
78 private static final List<Option> OPTIONS = ObjectUtils.notNull(
79 List.of(
80 AS_OPTION,
81 TO_OPTION,
82 OVERWRITE_OPTION));
83
84 @Override
85 public String getDescription() {
86 return "Resolve the specified OSCAL Profile";
87 }
88
89 @Override
90 public Collection<? extends Option> gatherOptions() {
91 return OPTIONS;
92 }
93
94 @Override
95 public List<ExtraArgument> getExtraArguments() {
96 return EXTRA_ARGUMENTS;
97 }
98
99 @SuppressWarnings({
100 "PMD.CyclomaticComplexity", "PMD.CognitiveComplexity",
101 "PMD.PreserveStackTrace"
102 })
103 @Override
104 public void validateOptions(CallingContext callingContext, CommandLine cmdLine) throws InvalidArgumentException {
105 if (cmdLine.hasOption(AS_OPTION)) {
106 try {
107 String toFormatText = cmdLine.getOptionValue(AS_OPTION);
108 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
109 } catch (IllegalArgumentException ex) {
110 InvalidArgumentException newEx = new InvalidArgumentException(
111 String.format("Invalid '%s' argument. The format must be one of: %s.",
112 OptionUtils.toArgument(AS_OPTION),
113 Arrays.asList(Format.values()).stream()
114 .map(Enum::name)
115 .collect(CustomCollectors.joiningWithOxfordComma("and"))));
116 newEx.setOption(AS_OPTION);
117 newEx.addSuppressed(ex);
118 throw newEx;
119 }
120 }
121
122 if (cmdLine.hasOption(TO_OPTION)) {
123 try {
124 String toFormatText = cmdLine.getOptionValue(TO_OPTION);
125 Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
126 } catch (IllegalArgumentException ex) {
127 InvalidArgumentException newEx
128 = new InvalidArgumentException("Invalid '--to' argument. The format must be one of: "
129 + Arrays.asList(Format.values()).stream()
130 .map(Enum::name)
131 .collect(CustomCollectors.joiningWithOxfordComma("and")));
132 newEx.setOption(AS_OPTION);
133 newEx.addSuppressed(ex);
134 throw newEx;
135 }
136 }
137
138 List<String> extraArgs = cmdLine.getArgList();
139 if (extraArgs.isEmpty()) {
140 throw new InvalidArgumentException("The source to resolve must be provided.");
141 }
142
143 File source = new File(extraArgs.get(0));
144 if (!source.exists()) {
145 throw new InvalidArgumentException("The provided source '" + source.getPath() + "' does not exist.");
146 }
147 if (!source.canRead()) {
148 throw new InvalidArgumentException("The provided source '" + source.getPath() + "' is not readable.");
149 }
150 }
151
152 @Override
153 public ICommandExecutor newExecutor(CallingContext callingContext, CommandLine cmdLine) {
154 return ICommandExecutor.using(callingContext, cmdLine, this::executeCommand);
155 }
156
157 @SuppressWarnings({
158 "PMD.OnlyOneReturn",
159 "unused"
160 })
161 protected ExitStatus executeCommand(
162 @NonNull CallingContext callingContext,
163 @NonNull CommandLine cmdLine) {
164 List<String> extraArgs = cmdLine.getArgList();
165 Path source = resolvePathAgainstCWD(ObjectUtils.notNull(Paths.get(extraArgs.get(0))));
166
167 IBoundLoader loader = OscalBindingContext.instance().newBoundLoader();
168 loader.disableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);
169
170 Format asFormat;
171
172 if (cmdLine.hasOption(AS_OPTION)) {
173 try {
174 String asFormatText = cmdLine.getOptionValue(AS_OPTION);
175 asFormat = Format.valueOf(asFormatText.toUpperCase(Locale.ROOT));
176 } catch (IllegalArgumentException ex) {
177 return ExitCode.INVALID_ARGUMENTS
178 .exitMessage("Invalid '--as' argument. The format must be one of: " + Arrays.stream(Format.values())
179 .map(Enum::name)
180 .collect(CustomCollectors.joiningWithOxfordComma("or")));
181 }
182 } else {
183
184 try {
185 asFormat = loader.detectFormat(ObjectUtils.notNull(source));
186 } catch (FileNotFoundException ex) {
187
188 return ExitCode.IO_ERROR.exitMessage("The provided source file '" + source + "' does not exist.");
189 } catch (IOException ex) {
190 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
191 } catch (IllegalArgumentException ex) {
192 return ExitCode.INVALID_ARGUMENTS.exitMessage(
193 "Source file has unrecognizable format. Use '--as' to specify the format. The format must be one of: "
194 + Arrays.stream(Format.values())
195 .map(Enum::name)
196 .collect(CustomCollectors.joiningWithOxfordComma("or")));
197 }
198 }
199
200 source = source.toAbsolutePath();
201 assert source != null;
202
203 Format toFormat;
204 if (cmdLine.hasOption(TO_OPTION)) {
205 String toFormatText = cmdLine.getOptionValue(TO_OPTION);
206 toFormat = Format.valueOf(toFormatText.toUpperCase(Locale.ROOT));
207 } else {
208 toFormat = asFormat;
209 }
210
211 Path destination = null;
212 if (extraArgs.size() == 2) {
213 destination = Paths.get(extraArgs.get(1)).toAbsolutePath();
214 }
215
216 if (destination != null) {
217 if (Files.exists(destination)) {
218 if (!cmdLine.hasOption(OVERWRITE_OPTION)) {
219 return ExitCode.INVALID_ARGUMENTS.exitMessage("The provided destination '" + destination
220 + "' already exists and the --overwrite option was not provided.");
221 }
222 if (!Files.isWritable(destination)) {
223 return ExitCode.IO_ERROR.exitMessage("The provided destination '" + destination + "' is not writable.");
224 }
225 } else {
226 Path parent = destination.getParent();
227 if (parent != null) {
228 try {
229 Files.createDirectories(parent);
230 } catch (IOException ex) {
231 return ExitCode.INVALID_TARGET.exit().withThrowable(ex);
232 }
233 }
234 }
235 }
236
237 IDocumentNodeItem document;
238 try {
239 document = loader.loadAsNodeItem(asFormat, source);
240 } catch (IOException ex) {
241 return ExitCode.IO_ERROR.exit().withThrowable(ex);
242 }
243 Object object = document.getValue();
244 if (object == null) {
245 return ExitCode.INVALID_ARGUMENTS.exitMessage("The target profile contained no data");
246 }
247
248 if (object instanceof Catalog) {
249
250 return ExitCode.INVALID_ARGUMENTS.exitMessage("The target is already a catalog");
251 }
252
253 if (!(object instanceof Profile)) {
254
255 return ExitCode.INVALID_ARGUMENTS.exitMessage("The target is not a profile");
256 }
257
258
259 DynamicContext dynamicContext = new DynamicContext(document.getStaticContext());
260 dynamicContext.setDocumentLoader(loader);
261 ProfileResolver resolver = new ProfileResolver();
262 resolver.setDynamicContext(dynamicContext);
263
264 IDocumentNodeItem resolvedProfile;
265 try {
266 resolvedProfile = resolver.resolve(document);
267 } catch (IOException | ProfileResolutionException ex) {
268 return ExitCode.PROCESSING_ERROR
269 .exitMessage(
270 String.format("Cmd: Unable to resolve profile '%s'. %s", document.getDocumentUri(), ex.getMessage()))
271 .withThrowable(ex);
272 }
273
274
275
276
277
278
279 ISerializer<Catalog> serializer
280 = OscalBindingContext.instance().newSerializer(toFormat, Catalog.class);
281 try {
282 if (destination == null) {
283 @SuppressWarnings({ "resource", "PMD.CloseResource" }) PrintStream stdOut = ObjectUtils.notNull(System.out);
284 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), stdOut);
285 } else {
286 serializer.serialize((Catalog) INodeItem.toValue(resolvedProfile), destination);
287 }
288 } catch (IOException ex) {
289 return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex);
290 }
291 return ExitCode.OK.exit();
292 }
293 }