diff --git a/cli-processor/src/main/java/gov/nist/secauto/metaschema/cli/processor/CLIProcessor.java b/cli-processor/src/main/java/gov/nist/secauto/metaschema/cli/processor/CLIProcessor.java index 36a318b26..042c260e0 100644 --- a/cli-processor/src/main/java/gov/nist/secauto/metaschema/cli/processor/CLIProcessor.java +++ b/cli-processor/src/main/java/gov/nist/secauto/metaschema/cli/processor/CLIProcessor.java @@ -110,12 +110,14 @@ public class CLIProcessor { SHOW_STACK_TRACE_OPTION, VERSION_OPTION); + public static final String COMMAND_VERSION = "http://csrc.nist.gov/ns/metaschema-java/cli/command-version"; + @NonNull private final List commands = new LinkedList<>(); @NonNull private final String exec; @NonNull - private final List versionInfos; + private final Map versionInfos; public static void main(String... args) { System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); @@ -130,10 +132,10 @@ public static void main(String... args) { @SuppressWarnings("null") public CLIProcessor(@NonNull String exec) { - this(exec, List.of()); + this(exec, Map.of()); } - public CLIProcessor(@NonNull String exec, @NonNull List versionInfos) { + public CLIProcessor(@NonNull String exec, @NonNull Map versionInfos) { this.exec = exec; this.versionInfos = versionInfos; AnsiConsole.systemInstall(); @@ -155,7 +157,7 @@ public String getExec() { * @return the versionInfo */ @NonNull - public List getVersionInfos() { + public Map getVersionInfos() { return versionInfos; } @@ -207,7 +209,6 @@ private static void handleNoColor() { AnsiConsole.systemUninstall(); } - @SuppressWarnings("resource") public static void handleQuiet() { LoggerContext ctx = (LoggerContext) LogManager.getContext(false); // NOPMD not closable here Configuration config = ctx.getConfiguration(); @@ -221,7 +222,7 @@ public static void handleQuiet() { protected void showVersion() { @SuppressWarnings("resource") PrintStream out = AnsiConsole.out(); // NOPMD - not owner - getVersionInfos().stream().forEach(info -> { + getVersionInfos().values().stream().forEach(info -> { out.println(ansi() .bold().a(info.getName()).boldOff() .a(" ") @@ -309,6 +310,11 @@ public CallingContext(@NonNull List args) { this.extraArgs = extraArgs; } + @NonNull + public CLIProcessor getCLIProcessor() { + return CLIProcessor.this; + } + @Nullable public ICommand getTargetCommand() { return calledCommands.peekLast(); diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/node/AbstractFlagInstanceNodeItem.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/node/AbstractFlagInstanceNodeItem.java index 067568f7a..5813fe1bf 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/node/AbstractFlagInstanceNodeItem.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/item/node/AbstractFlagInstanceNodeItem.java @@ -30,8 +30,6 @@ import gov.nist.secauto.metaschema.core.model.IFlagDefinition; import gov.nist.secauto.metaschema.core.model.IFlagInstance; -import java.net.URI; - import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -72,11 +70,6 @@ public IFlagInstance getInstance() { return parent; } - @Override - public URI getBaseUri() { - return getDefinition().getContainingModule().getLocation(); - } - @Override public String toString() { StringBuilder builder = new StringBuilder() diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ConstraintValidationFinding.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ConstraintValidationFinding.java index 92324c199..6317ebdad 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ConstraintValidationFinding.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ConstraintValidationFinding.java @@ -149,10 +149,9 @@ public Level getSeverity() { return severity; } - @SuppressWarnings("null") @Override - public @NonNull URI getDocumentUri() { - return getNode().getBaseUri(); + public URI getDocumentUri() { + return getTarget().getBaseUri(); } @NonNull diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java index f01a15b6d..0b0729419 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java @@ -235,6 +235,7 @@ public void handleAllowedValuesViolation( public void handleIndexDuplicateViolation(IIndexConstraint constraint, INodeItem node) { addFinding(ConstraintValidationFinding.builder(constraint, node) .kind(Kind.FAIL) + .target(node) .message(newIndexDuplicateViolationMessage(constraint, node)) .severity(Level.CRITICAL) .build()); @@ -245,6 +246,7 @@ public void handleIndexMiss(IIndexHasKeyConstraint constraint, INodeItem node, I addFinding(ConstraintValidationFinding.builder(constraint, node) .severity(constraint.getLevel()) .kind(toKind(constraint.getLevel())) + .target(target) .message(newIndexMissMessage(constraint, node, target, key)) .build()); } @@ -255,6 +257,7 @@ public void handleGenericValidationViolation(IConstraint constraint, INodeItem n addFinding(ConstraintValidationFinding.builder(constraint, node) .severity(constraint.getLevel()) .kind(toKind(constraint.getLevel())) + .target(target) .message(newGenericValidationViolationMessage(constraint, node, target, message)) .build()); } @@ -262,6 +265,7 @@ public void handleGenericValidationViolation(IConstraint constraint, INodeItem n @Override public void handlePass(IConstraint constraint, INodeItem node, INodeItem target) { addFinding(ConstraintValidationFinding.builder(constraint, node) + .target(target) .severity(Level.NONE) .kind(Kind.PASS) .build()); diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/validation/IValidationFinding.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/validation/IValidationFinding.java index a659837b7..a37e929f9 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/validation/IValidationFinding.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/validation/IValidationFinding.java @@ -63,9 +63,9 @@ enum Kind { /** * Get the document's URI. * - * @return the document's URI + * @return the document's URI or {@code null} if it is not known */ - @NonNull + @Nullable URI getDocumentUri(); /** diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/util/UriUtils.java b/core/src/main/java/gov/nist/secauto/metaschema/core/util/UriUtils.java index 60a99eb50..6d7dada9c 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/util/UriUtils.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/util/UriUtils.java @@ -31,10 +31,15 @@ import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; +import java.util.regex.Pattern; import edu.umd.cs.findbugs.annotations.NonNull; public final class UriUtils { + private static final Pattern URI_SEPERATOR_PATTERN = Pattern.compile("\\/"); + private static final String URI_SEPERATOR = "/"; private UriUtils() { // disable construction @@ -71,4 +76,92 @@ public static URI toUri(@NonNull String location, @NonNull URI baseUri) throws U } return baseUri.resolve(asUri.normalize()); } + + /** + * This function extends the functionality of {@link URI#relativize(URI)} by + * supporting relative reference pathing (e.g., ..), when the {@code prepend} + * parameter is set to {@code true}. + * + * @param base + * the URI to relativize against + * @param other + * the URI to make relative + * @param prepend + * if {@code true}, then prepend relative pathing + * @return a new relative URI + * @throws URISyntaxException + * if any of the URIs are malformed + */ + public static URI relativize(URI base, URI other, boolean prepend) throws URISyntaxException { + URI normBase = Objects.requireNonNull(base).normalize(); + URI normOther = Objects.requireNonNull(other).normalize(); + URI retval = normBase.relativize(normOther); + + if (prepend && !normBase.isOpaque() && !retval.isOpaque() && hasSameSchemeAndAuthority(normBase, retval)) { + // the URIs are not opaque and they share the same scheme and authority + String basePath = normBase.getPath(); + String targetPath = normOther.getPath(); + String newPath = prependRelativePath(basePath, targetPath); + + retval = new URI(null, null, newPath, normOther.getQuery(), normOther.getFragment()); + } + + return retval; + } + + private static boolean hasSameSchemeAndAuthority(URI base, URI other) { + String baseScheme = base.getScheme(); + boolean retval = (baseScheme == null && other.getScheme() == null) + || (baseScheme != null && baseScheme.equals(other.getScheme())); + String baseAuthority = base.getAuthority(); + retval = retval && ((baseAuthority == null && other.getAuthority() == null) + || (baseAuthority != null && baseAuthority.equals(other.getAuthority()))); + return retval; + } + + /** + * Based on code from + * http://stackoverflow.com/questions/10801283/get-relative-path-of-two-uris-in-java + * + * @param base + * the base path to resolve against + * @param target + * the URI to relativize against the base + * @return the relativized URI + */ + public static String prependRelativePath(String base, String target) { + + // Split paths into segments + String[] baseSegments = URI_SEPERATOR_PATTERN.split(base); + String[] targetSegments = URI_SEPERATOR_PATTERN.split(target, -1); + + // Discard trailing segment of base path, since this resource doesn't matter + if (baseSegments.length > 0 && !base.endsWith(URI_SEPERATOR)) { + baseSegments = Arrays.copyOf(baseSegments, baseSegments.length - 1); + } + + // Remove common prefix segments + int segmentIndex = 0; + while (segmentIndex < baseSegments.length && segmentIndex < targetSegments.length + && baseSegments[segmentIndex].equals(targetSegments[segmentIndex])) { + segmentIndex++; + } + + // Construct the relative path + StringBuilder retval = new StringBuilder(); + for (int j = 0; j < (baseSegments.length - segmentIndex); j++) { + retval.append(".."); + if (retval.length() != 0) { + retval.append(URI_SEPERATOR); + } + } + + for (int j = segmentIndex; j < targetSegments.length; j++) { + retval.append(targetSegments[j]); + if (retval.length() != 0 && j < targetSegments.length - 1) { + retval.append(URI_SEPERATOR); + } + } + return retval.toString(); + } } diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/util/UriUtilsTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/util/UriUtilsTest.java index 62ce23b62..1ead33a92 100644 --- a/core/src/test/java/gov/nist/secauto/metaschema/core/util/UriUtilsTest.java +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/util/UriUtilsTest.java @@ -33,6 +33,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.stream.Stream; @@ -61,16 +62,72 @@ private static Stream provideValuesTestToUri() { @ParameterizedTest @MethodSource("provideValuesTestToUri") - void testToUri(@NonNull String location, boolean expectedResult) { + void testToUri(@NonNull String location, boolean expectedResult) throws URISyntaxException { boolean result = INVALID; Path cwd = Paths.get(""); - try { - URI uri = UriUtils.toUri(location, cwd.toAbsolutePath().toUri()); - result = VALID; - System.out.println(String.format("%s -> %s", location, uri.toASCIIString())); - } catch (Exception ex) { - ex.printStackTrace(); - } + URI uri = UriUtils.toUri(location, cwd.toAbsolutePath().toUri()); + result = VALID; + // System.out.println(String.format("%s -> %s", location, uri.toASCIIString())); assertEquals(result, expectedResult); } + + private static Stream provideArgumentsTestRelativize() { + return Stream.of( + Arguments.of( + "http://example.com/this/file1.txt", + "http://example.com/this/file2.txt", + true, + "file2.txt"), + Arguments.of( + "http://example.com/this", + "http://example.com/this/that", + true, + "that"), + Arguments.of( + "http://example.com/this/", + "http://example.com/this/that", + true, + "that"), + Arguments.of( + "http://example.com/this/that", + "http://example.com/this/new", + true, + "new"), + Arguments.of( + "http://example.com/this/that/A", + "http://example.com/this/new/B", + true, + "../new/B"), + Arguments.of( + "http://example.com/this/that/", + "http://example.com/this/new/", + true, + "../new/"), + Arguments.of( + "http://example.com/this/that/A/", + "http://example.com/this/new/B", + true, + "../../new/B"), + Arguments.of( + "http://example.com/this/that/A/X/file1,text", + "http://example.com/this/that/A/file2.txt", + true, + "../file2.txt"), + Arguments.of( + "http://example.com/this/that/A/", + "http://example.org/this/new/B", + true, + "http://example.org/this/new/B")); + } + + @ParameterizedTest + @MethodSource("provideArgumentsTestRelativize") + void testRelativize(@NonNull String uri1, @NonNull String uri2, boolean prepend, @NonNull String expected) + throws URISyntaxException { + URI thisUri = URI.create(uri1); + URI thatUri = URI.create(uri2); + + URI result = UriUtils.relativize(thisUri, thatUri, prepend); + assertEquals(expected, result.toASCIIString()); + } } diff --git a/databind-metaschema/src/main/java/gov/nist/secauto/metaschema/modules/sarif/SarifValidationHandler.java b/databind-metaschema/src/main/java/gov/nist/secauto/metaschema/modules/sarif/SarifValidationHandler.java index 58e3df306..5e9a905e6 100644 --- a/databind-metaschema/src/main/java/gov/nist/secauto/metaschema/modules/sarif/SarifValidationHandler.java +++ b/databind-metaschema/src/main/java/gov/nist/secauto/metaschema/modules/sarif/SarifValidationHandler.java @@ -26,35 +26,50 @@ package gov.nist.secauto.metaschema.modules.sarif; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Location; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Message; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.PhysicalLocation; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Region; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Result; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Run; -import gov.nist.csrc.ns.oscal.metaschema.validation.results.x10.Sarif; import gov.nist.secauto.metaschema.core.model.IResourceLocation; import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding; -import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding; import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding; +import gov.nist.secauto.metaschema.core.util.CollectionUtil; +import gov.nist.secauto.metaschema.core.util.IVersionInfo; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; +import gov.nist.secauto.metaschema.core.util.UriUtils; import gov.nist.secauto.metaschema.databind.IBindingContext; import gov.nist.secauto.metaschema.databind.io.Format; import gov.nist.secauto.metaschema.databind.io.SerializationFeature; +import org.schemastore.json.sarif.x210.Artifact; +import org.schemastore.json.sarif.x210.ArtifactLocation; +import org.schemastore.json.sarif.x210.Location; +import org.schemastore.json.sarif.x210.LogicalLocation; +import org.schemastore.json.sarif.x210.Message; +import org.schemastore.json.sarif.x210.PhysicalLocation; +import org.schemastore.json.sarif.x210.Region; +import org.schemastore.json.sarif.x210.ReportingDescriptor; +import org.schemastore.json.sarif.x210.Result; +import org.schemastore.json.sarif.x210.Run; +import org.schemastore.json.sarif.x210.Sarif; +import org.schemastore.json.sarif.x210.Tool; +import org.schemastore.json.sarif.x210.ToolComponent; + import java.io.IOException; import java.math.BigInteger; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import edu.umd.cs.findbugs.annotations.Nullable; public final class SarifValidationHandler { private enum Kind { @@ -97,33 +112,134 @@ public String getLabel() { } } - private static final SarifValidationHandler INSTANCE = new SarifValidationHandler(); + @NonNull + private final URI source; + @Nullable + private final IVersionInfo toolVersion; + private final AtomicInteger artifactIndex = new AtomicInteger(-1); + @NonNull + private final Map artifacts = new LinkedHashMap<>(); + @NonNull + private final Map rules = new LinkedHashMap<>(); + @NonNull + private final List results = new LinkedList<>(); + + public SarifValidationHandler( + @NonNull URI source, + @Nullable IVersionInfo toolVersion) { + if (!source.isAbsolute()) { + throw new IllegalArgumentException(String.format("The source URI '%s' is not absolute.", source.toASCIIString())); + } + + this.source = source; + this.toolVersion = toolVersion; + } - @SuppressFBWarnings(value = "SING_SINGLETON_GETTER_NOT_SYNCHRONIZED", - justification = "both values are class initialized") - public static SarifValidationHandler instance() { - return INSTANCE; + public URI getSource() { + return source; } - private SarifValidationHandler() { - // disable construction + public IVersionInfo getToolVersion() { + return toolVersion; } - public boolean handleValidationResults( - @NonNull URI source, - @NonNull Path outputFile, - @NonNull IValidationResult validationResult, - @NonNull IBindingContext bindingContext) throws IOException { + public void addFindings(@NonNull List findings) { + for (IValidationFinding finding : findings) { + assert finding != null; + addFinding(finding); + } + } + + public void addFinding(@NonNull IValidationFinding finding) { + if (finding instanceof JsonValidationFinding) { + addJsonValidationFinding((JsonValidationFinding) finding); + } else if (finding instanceof XmlValidationFinding) { + addXmlValidationFinding((XmlValidationFinding) finding); + } else if (finding instanceof ConstraintValidationFinding) { + addConstraintValidationFinding((ConstraintValidationFinding) finding); + } else { + throw new IllegalStateException(); + } + } + + public URI relativize(@NonNull URI output, @NonNull URI artifact) throws IOException { + try { + return UriUtils.relativize(output, artifact, true); + } catch (URISyntaxException ex) { + throw new IOException(ex); + } + } + + private RuleRecord getRuleRecord(@NonNull IConstraint constraint) { + RuleRecord retval = rules.get(constraint); + if (retval == null) { + retval = new RuleRecord(constraint); + rules.put(constraint, retval); + } + return retval; + } + + private ArtifactRecord getArtifactRecord(@NonNull URI artifactUri) { + ArtifactRecord retval = artifacts.get(artifactUri); + if (retval == null) { + retval = new ArtifactRecord(artifactUri); + artifacts.put(artifactUri, retval); + } + return retval; + } + + private void addJsonValidationFinding(@NonNull JsonValidationFinding finding) { + results.add(new SchemaResult(finding)); + } + + private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) { + results.add(new SchemaResult(finding)); + } + + private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) { + results.add(new ConstraintResult(finding)); + } + + public void write(@NonNull Path outputFile) throws IOException { + + URI output = outputFile.toUri(); Sarif sarif = new Sarif(); sarif.setVersion("2.1.0"); Run run = new Run(); + sarif.addRun(run); - handleValidationFindings(validationResult.getFindings(), run); + Artifact artifact = new Artifact(); + + artifact.setLocation(getArtifactRecord(source).generateArtifactLocation(output)); + + run.addArtifact(artifact); + + for (IResult result : results) { + result.generateResults(output).forEach(run::addResult); + } - bindingContext.newSerializer(Format.JSON, Sarif.class) + if (!rules.isEmpty() || toolVersion != null) { + Tool tool = new Tool(); + ToolComponent driver = new ToolComponent(); + + IVersionInfo toolVersion = getToolVersion(); + if (toolVersion != null) { + driver.setName(toolVersion.getName()); + driver.setVersion(toolVersion.getVersion()); + } + + for (RuleRecord rule : rules.values()) { + driver.addRule(rule(rule)); + } + + tool.setDriver(driver); + run.setTool(tool); + } + + IBindingContext.instance().newSerializer(Format.JSON, Sarif.class) .disableFeature(SerializationFeature.SERIALIZE_ROOT) .serialize( sarif, @@ -131,236 +247,243 @@ public boolean handleValidationResults( StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); - - return validationResult.isPassing(); } - public void handleValidationFindings( - @NonNull List findings, - @NonNull Run run) { - - for (IValidationFinding finding : findings) { - if (finding instanceof JsonValidationFinding) { - run.addResult(handleJsonValidationFinding((JsonValidationFinding) finding)); - } else if (finding instanceof XmlValidationFinding) { - run.addResult(handleXmlValidationFinding((XmlValidationFinding) finding)); - } else if (finding instanceof ConstraintValidationFinding) { - handleConstraintValidationFinding((ConstraintValidationFinding) finding).stream() - .forEachOrdered(run::addResult); - } else { - throw new IllegalStateException(); - } + private ReportingDescriptor rule(RuleRecord rule) { + ReportingDescriptor retval = new ReportingDescriptor(); + retval.setId(rule.getId()); + String name = rule.getConstraint().getId(); + if (name != null) { + retval.setName(name); } + return retval; } - private Result handleJsonValidationFinding(@NonNull JsonValidationFinding finding) { - Result result = new Result(); - - result.setKind(kind(finding).getLabel()); - result.setLevel(level(finding.getSeverity()).getLabel()); - message(finding, result); - location(finding, result); - // retval.setMessage(message(finding.getMessage())); - // - // - // getLogger(finding).log( - // ansi.a('[') - // .a(finding.getCause().getPointerToViolation()) - // .reset() - // .a(']') - // .format(" %s [%s]", - // finding.getMessage(), - // finding.getDocumentUri().toString())); - - return result; - } + private interface IResult { + @NonNull + IValidationFinding getFinding(); - @NonNull - private Kind kind(@NonNull IValidationFinding finding) { - IValidationFinding.Kind kind = finding.getKind(); - - Kind retval; - switch (kind) { - case FAIL: - retval = Kind.FAIL; - break; - case INFORMATIONAL: - retval = Kind.INFORMATIONAL; - break; - case NOT_APPLICABLE: - retval = Kind.NOT_APPLICABLE; - break; - case PASS: - retval = Kind.PASS; - break; - default: - throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind)); - } - return retval; + @NonNull + List generateResults(@NonNull URI output) throws IOException; } - @NonNull - private SeverityLevel level(@NonNull Level severity) { - SeverityLevel retval; - switch (severity) { - case CRITICAL: - case ERROR: - retval = SeverityLevel.ERROR; - break; - case INFORMATIONAL: - case DEBUG: - retval = SeverityLevel.NOTE; - break; - case WARNING: - retval = SeverityLevel.WARNING; - break; - case NONE: - retval = SeverityLevel.NONE; - break; - default: - throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity)); + private abstract class AbstractResult implements IResult { + @NonNull + private final T finding; + + protected AbstractResult(@NonNull T finding) { + this.finding = finding; } - return retval; - } - private void message(@NonNull IValidationFinding finding, @NonNull Result result) { - String message = finding.getMessage(); - if (message != null) { - Message msg = new Message(); - msg.setText(message); - result.setMessage(msg); + @Override + public T getFinding() { + return finding; } - } - private void location(@NonNull IValidationFinding finding, @NonNull Result result) { - IResourceLocation location = finding.getLocation(); - if (location != null) { - Region region = new Region(); + @NonNull + protected Kind kind(@NonNull IValidationFinding finding) { + IValidationFinding.Kind kind = finding.getKind(); + + Kind retval; + switch (kind) { + case FAIL: + retval = Kind.FAIL; + break; + case INFORMATIONAL: + retval = Kind.INFORMATIONAL; + break; + case NOT_APPLICABLE: + retval = Kind.NOT_APPLICABLE; + break; + case PASS: + retval = Kind.PASS; + break; + default: + throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind)); + } + return retval; + } - if (location.getLine() > -1) { - region.setStartLine(BigInteger.valueOf(location.getLine())); + @NonNull + protected SeverityLevel level(@NonNull Level severity) { + SeverityLevel retval; + switch (severity) { + case CRITICAL: + case ERROR: + retval = SeverityLevel.ERROR; + break; + case INFORMATIONAL: + case DEBUG: + retval = SeverityLevel.NOTE; + break; + case WARNING: + retval = SeverityLevel.WARNING; + break; + case NONE: + retval = SeverityLevel.NONE; + break; + default: + throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity)); } - if (location.getColumn() > -1) { - region.setStartColumn(BigInteger.valueOf(location.getColumn())); + return retval; + } + + protected void message(@NonNull IValidationFinding finding, @NonNull Result result) { + String message = finding.getMessage(); + if (message == null) { + message = ""; } - if (location.getByteOffset() > -1) { - region.setByteOffset(BigInteger.valueOf(location.getByteOffset())); + if (message != null) { + Message msg = new Message(); + msg.setText(message); + result.setMessage(msg); } - if (location.getCharOffset() > -1) { - region.setCharOffset(BigInteger.valueOf(location.getCharOffset())); + } + + protected void location(@NonNull IValidationFinding finding, @NonNull Result result, @NonNull URI base) + throws IOException { + IResourceLocation location = finding.getLocation(); + if (location != null) { + // region + Region region = new Region(); + + if (location.getLine() > -1) { + region.setStartLine(BigInteger.valueOf(location.getLine())); + region.setEndLine(BigInteger.valueOf(location.getLine())); + } + if (location.getColumn() > -1) { + region.setStartColumn(BigInteger.valueOf(location.getColumn())); + region.setEndColumn(BigInteger.valueOf(location.getColumn() + 1)); + } + if (location.getByteOffset() > -1) { + region.setByteOffset(BigInteger.valueOf(location.getByteOffset())); + region.setByteLength(BigInteger.ZERO); + } + if (location.getCharOffset() > -1) { + region.setCharOffset(BigInteger.valueOf(location.getCharOffset())); + region.setCharLength(BigInteger.ZERO); + } + + PhysicalLocation physical = new PhysicalLocation(); + + URI documentUri = finding.getDocumentUri(); + if (documentUri != null) { + physical.setArtifactLocation(getArtifactRecord(documentUri).generateArtifactLocation(base)); + } + physical.setRegion(region); + + LogicalLocation logical = new LogicalLocation(); + + logical.setDecoratedName(finding.getPath()); + + Location loc = new Location(); + loc.setPhysicalLocation(physical); + loc.setLogicalLocation(logical); + result.addLocation(loc); } + } + } - PhysicalLocation physical = new PhysicalLocation(); - physical.setRegion(region); + private class SchemaResult + extends AbstractResult { - Location loc = new Location(); - loc.setPhysicalLocation(physical); - result.setLocation(loc); + protected SchemaResult(@NonNull IValidationFinding finding) { + super(finding); } - } - private Result handleXmlValidationFinding(@NonNull XmlValidationFinding finding) { - Result result = new Result(); + @Override + public List generateResults(@NonNull URI output) throws IOException { + IValidationFinding finding = getFinding(); - result.setKind(kind(finding).getLabel()); - result.setLevel(level(finding.getSeverity()).getLabel()); - message(finding, result); - location(finding, result); + Result result = new Result(); - // SAXParseException ex = finding.getCause(); - // - // getLogger(finding).log( - // ansi.format("%s [%s{%d,%d}]", - // finding.getMessage(), - // finding.getDocumentUri().toString(), - // ex.getLineNumber(), - // ex.getColumnNumber())); + result.setKind(kind(finding).getLabel()); + result.setLevel(level(finding.getSeverity()).getLabel()); + message(finding, result); + location(finding, result, output); - return result; + return CollectionUtil.singletonList(result); + } } - private List handleConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) { - List retval = new LinkedList<>(); + private class ConstraintResult + extends AbstractResult { - Kind kind = kind(finding); - SeverityLevel level = level(finding.getSeverity()); + protected ConstraintResult(@NonNull ConstraintValidationFinding finding) { + super(finding); + } - for (IConstraint constraint : finding.getConstraints()) { + @Override + public List generateResults(@NonNull URI output) throws IOException { + ConstraintValidationFinding finding = getFinding(); - Result result = new Result(); + List retval = new LinkedList<>(); - String id = constraint.getId(); - if (id != null) { - result.setRuleId(id); + Kind kind = kind(finding); + SeverityLevel level = level(finding.getSeverity()); + + for (IConstraint constraint : finding.getConstraints()) { + assert constraint != null; + RuleRecord rule = getRuleRecord(constraint); + + Result result = new Result(); + + result.setRuleId(rule.getId()); + result.setKind(kind.getLabel()); + result.setLevel(level.getLabel()); + message(finding, result); + location(finding, result, output); + + retval.add(result); } - result.setKind(kind.getLabel()); - result.setLevel(level.getLabel()); - message(finding, result); - location(finding, result); + return retval; + } + } - // getLogger(finding).log( - // ansi.format("[%s] %s", finding.getNode().getMetapath(), - // finding.getMessage())); + private static class RuleRecord { + @NonNull + private final String id; + @NonNull + private final IConstraint constraint; - retval.add(result); + public RuleRecord(@NonNull IConstraint constraint) { + this.id = ObjectUtils.notNull(UUID.randomUUID().toString()); + this.constraint = constraint; + } + + @NonNull + public String getId() { + return id; + } + + public IConstraint getConstraint() { + return constraint; + } + } + + private class ArtifactRecord { + private final URI uri; + private final int index; + + public ArtifactRecord(@NonNull URI uri) { + this.uri = uri; + this.index = artifactIndex.addAndGet(1); + } + + @NonNull + public URI getUri() { + return uri; + } + + public int getIndex() { + return index; + } + + public ArtifactLocation generateArtifactLocation(@NonNull URI baseUri) throws IOException { + ArtifactLocation location = new ArtifactLocation(); + location.setUri(relativize(baseUri, source)); + location.setIndex(BigInteger.valueOf(getIndex())); + return location; } - return retval; } - // - // @NonNull - // private LogBuilder getLogger(@NonNull IValidationFinding finding) { - // LogBuilder retval; - // switch (finding.getSeverity()) { - // case CRITICAL: - // retval = LOGGER.atFatal(); - // break; - // case ERROR: - // retval = LOGGER.atError(); - // break; - // case WARNING: - // retval = LOGGER.atWarn(); - // break; - // case INFORMATIONAL: - // retval = LOGGER.atInfo(); - // break; - // default: - // throw new IllegalArgumentException("Unknown level: " + - // finding.getSeverity().name()); - // } - // - // assert retval != null; - // - // if (finding.getCause() != null && isLogExceptions()) { - // retval.withThrowable(finding.getCause()); - // } - // - // return retval; - // } - // - // @SuppressWarnings("static-method") - // @NonNull - // private Ansi generatePreamble(@NonNull Level level) { - // - // switch (level) { - // case CRITICAL: - // ansi = ansi.fgRed().a("CRITICAL").reset(); - // break; - // case ERROR: - // ansi = ansi.fgBrightRed().a("ERROR").reset(); - // break; - // case WARNING: - // ansi = ansi.fgBrightYellow().a("WARNING").reset(); - // break; - // case INFORMATIONAL: - // ansi = ansi.fgBrightBlue().a("INFO").reset(); - // break; - // default: - // ansi = ansi().a(level.name()).reset(); - // break; - // } - // ansi = ansi.a("] ").reset(); - // - // assert ansi != null; - // return ansi; - // } } diff --git a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/CLI.java b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/CLI.java index 794ad7481..819615f9f 100644 --- a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/CLI.java +++ b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/CLI.java @@ -35,7 +35,8 @@ import gov.nist.secauto.metaschema.core.util.IVersionInfo; import gov.nist.secauto.metaschema.core.util.ObjectUtils; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import edu.umd.cs.findbugs.annotations.NonNull; @@ -49,10 +50,13 @@ public static void main(String[] args) { public static ExitStatus runCli(String... args) { System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - List versions = ObjectUtils.notNull( - List.of( - new MetaschemaJavaVersion(), - new MetaschemaVersion())); + Map versions = ObjectUtils.notNull( + new LinkedHashMap<>() { + { + put(CLIProcessor.COMMAND_VERSION, new MetaschemaJavaVersion()); + put("http://csrc.nist.gov/ns/oscal/metaschema/1.0", new MetaschemaVersion()); + } + }); CLIProcessor processor = new CLIProcessor("metaschema-cli", versions); MetaschemaCommands.COMMANDS.forEach(processor::addCommandHandler); diff --git a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/commands/AbstractValidateContentCommand.java b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/commands/AbstractValidateContentCommand.java index 006a22ca5..b5f14af4e 100644 --- a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/commands/AbstractValidateContentCommand.java +++ b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/commands/AbstractValidateContentCommand.java @@ -46,6 +46,7 @@ import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; import gov.nist.secauto.metaschema.core.util.CollectionUtil; import gov.nist.secauto.metaschema.core.util.CustomCollectors; +import gov.nist.secauto.metaschema.core.util.IVersionInfo; import gov.nist.secauto.metaschema.core.util.ObjectUtils; import gov.nist.secauto.metaschema.core.util.UriUtils; import gov.nist.secauto.metaschema.databind.IBindingContext; @@ -268,9 +269,16 @@ public ExitStatus execute() { if (cmdLine.hasOption(OUTPUT_FILE_OPTION) && LOGGER.isInfoEnabled()) { Path sarifFile = Paths.get(cmdLine.getOptionValue(OUTPUT_FILE_OPTION)); + + IVersionInfo version + = getCallingContext().getCLIProcessor().getVersionInfos().get(CLIProcessor.COMMAND_VERSION); + try { - SarifValidationHandler.instance().handleValidationResults(source, sarifFile, validationResult, - bindingContext); + SarifValidationHandler sarifHandler = new SarifValidationHandler(source, version); + sarifHandler.addFindings(validationResult.getFindings()); + sarifHandler.write(sarifFile); + + LOGGER.error("The file '{}' is invalid.", source); } catch (IOException ex) { return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex); } diff --git a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/util/LoggingValidationHandler.java b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/util/LoggingValidationHandler.java index 75a334f92..e0f9e8338 100644 --- a/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/util/LoggingValidationHandler.java +++ b/metaschema-cli/src/main/java/gov/nist/secauto/metaschema/cli/util/LoggingValidationHandler.java @@ -42,6 +42,7 @@ import org.fusesource.jansi.Ansi.Color; import org.xml.sax.SAXParseException; +import java.net.URI; import java.util.List; import edu.umd.cs.findbugs.annotations.NonNull; @@ -95,27 +96,37 @@ public void handleValidationFindings(@NonNull List private void handleJsonValidationFinding(@NonNull JsonValidationFinding finding) { Ansi ansi = generatePreamble(finding.getSeverity()); - getLogger(finding).log( - ansi.a('[') - .fgBright(Color.WHITE) - .a(finding.getCause().getPointerToViolation()) - .reset() - .a(']') - .format(" %s [%s]", - finding.getMessage(), - finding.getDocumentUri().toString())); + ansi = ansi.a('[') + .fgBright(Color.WHITE) + .a(finding.getCause().getPointerToViolation()) + .reset() + .a(']'); + + URI documentUri = finding.getDocumentUri(); + ansi = documentUri == null + ? ansi.format(" %s", finding.getMessage()) + : ansi.format(" %s [%s]", finding.getMessage(), documentUri.toString()); + + getLogger(finding).log(ansi); } private void handleXmlValidationFinding(XmlValidationFinding finding) { Ansi ansi = generatePreamble(finding.getSeverity()); SAXParseException ex = finding.getCause(); - getLogger(finding).log( - ansi.format("%s [%s{%d,%d}]", + URI documentUri = finding.getDocumentUri(); + ansi = documentUri == null + ? ansi.format("%s [{%d,%d}]", finding.getMessage(), - finding.getDocumentUri().toString(), ex.getLineNumber(), - ex.getColumnNumber())); + ex.getColumnNumber()) + : ansi.format("%s [%s{%d,%d}]", + finding.getMessage(), + documentUri.toString(), + ex.getLineNumber(), + ex.getColumnNumber()); + + getLogger(finding).log(ansi); } private void handleConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {