SarifValidationHandler.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.modules.sarif;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
import gov.nist.secauto.metaschema.core.model.IAttributable;
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.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.MultiformatMessageString;
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.SarifModule;
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.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
/**
* Supports building a Static Analysis Results Interchange Format (SARIF)
* document based on a set of validation findings.
*/
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class SarifValidationHandler {
private enum Kind {
NOT_APPLICABLE("notApplicable"),
PASS("pass"),
FAIL("fail"),
REVIEW("review"),
OPEN("open"),
INFORMATIONAL("informational");
@NonNull
private final String label;
Kind(@NonNull String label) {
this.label = label;
}
@NonNull
public String getLabel() {
return label;
}
}
private enum SeverityLevel {
NONE("none"),
NOTE("note"),
WARNING("warning"),
ERROR("error");
@NonNull
private final String label;
SeverityLevel(@NonNull String label) {
this.label = label;
}
@NonNull
public String getLabel() {
return label;
}
}
@NonNull
static final String SARIF_NS = "https://docs.oasis-open.org/sarif/sarif/v2.1.0";
@NonNull
static final IAttributable.Key SARIF_HELP_URL_KEY
= IAttributable.key("help-url", SARIF_NS);
@NonNull
static final IAttributable.Key SARIF_HELP_TEXT_KEY
= IAttributable.key("help-text", SARIF_NS);
@NonNull
static final IAttributable.Key SARIF_HELP_MARKDOWN_KEY
= IAttributable.key("help-markdown", SARIF_NS);
@NonNull
private final URI source;
@Nullable
private final IVersionInfo toolVersion;
private final AtomicInteger artifactIndex = new AtomicInteger(-1);
private final AtomicInteger ruleIndex = new AtomicInteger(-1);
@SuppressWarnings("PMD.UseConcurrentHashMap")
@NonNull
private final Map<URI, ArtifactRecord> artifacts = new LinkedHashMap<>();
@NonNull
private final List<AbstractRuleRecord> rules = new LinkedList<>();
@SuppressWarnings("PMD.UseConcurrentHashMap")
@NonNull
private final Map<IConstraint, ConstraintRuleRecord> constraintRules = new LinkedHashMap<>();
@NonNull
private final List<IResult> results = new LinkedList<>();
@NonNull
private final SchemaRuleRecord schemaRule = new SchemaRuleRecord();
private boolean schemaValid = true;
/**
* Construct a new validation handler.
*
* @param source
* the URI of the content that was validated
* @param toolVersion
* the version information for the tool producing the validation
* results
*/
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;
}
@NonNull
private URI getSource() {
return source;
}
private IVersionInfo getToolVersion() {
return toolVersion;
}
/**
* Register a collection of validation finding.
*
* @param findings
* the findings to register
*/
public void addFindings(@NonNull Collection<? extends IValidationFinding> findings) {
for (IValidationFinding finding : findings) {
assert finding != null;
addFinding(finding);
}
}
/**
* Register a validation finding.
*
* @param finding
* the finding to register
*/
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();
}
}
private ConstraintRuleRecord getRuleRecord(@NonNull IConstraint constraint) {
ConstraintRuleRecord retval = constraintRules.get(constraint);
if (retval == null) {
retval = new ConstraintRuleRecord(constraint);
constraintRules.put(constraint, retval);
rules.add(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));
if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
schemaValid = false;
}
}
private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) {
results.add(new SchemaResult(finding));
if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
schemaValid = false;
}
}
private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {
results.add(new ConstraintResult(finding));
}
/**
* Write the collection of findings to the provided output file.
*
* @param outputFile
* the path to the output file to write to
* @param bindingContext
* the context used to access Metaschema module information based on
* Java class bindings
* @throws IOException
* if an error occurred while writing the SARIF file
*/
public void write(
@NonNull Path outputFile,
@NonNull IBindingContext bindingContext) throws IOException {
URI output = ObjectUtils.notNull(outputFile.toUri());
Sarif sarif = new Sarif();
sarif.setVersion("2.1.0");
Run run = new Run();
sarif.addRun(run);
Artifact artifact = new Artifact();
artifact.setLocation(getArtifactRecord(getSource()).generateArtifactLocation(output));
run.addArtifact(artifact);
for (IResult result : results) {
result.generateResults(output).forEach(run::addResult);
}
IVersionInfo toolVersion = getToolVersion();
if (!rules.isEmpty() || toolVersion != null) {
Tool tool = new Tool();
ToolComponent driver = new ToolComponent();
if (toolVersion != null) {
driver.setName(toolVersion.getName());
driver.setVersion(toolVersion.getVersion());
}
for (AbstractRuleRecord rule : rules) {
driver.addRule(rule.generate());
}
tool.setDriver(driver);
run.setTool(tool);
}
bindingContext.registerModule(SarifModule.class);
bindingContext.newSerializer(Format.JSON, Sarif.class)
.disableFeature(SerializationFeature.SERIALIZE_ROOT)
.serialize(
sarif,
outputFile,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
}
private interface IResult {
@NonNull
IValidationFinding getFinding();
@NonNull
List<Result> generateResults(@NonNull URI output) throws IOException;
}
private abstract class AbstractResult<T extends IValidationFinding> implements IResult {
@NonNull
private final T finding;
protected AbstractResult(@NonNull T finding) {
this.finding = finding;
}
@Override
public T getFinding() {
return finding;
}
@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;
}
@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));
}
return retval;
}
protected void message(@NonNull IValidationFinding finding, @NonNull Result result) {
String message = finding.getMessage();
if (message == null) {
message = "";
}
Message msg = new Message();
msg.setText(message);
result.setMessage(msg);
}
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() + 1));
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);
}
}
}
private final class SchemaResult
extends AbstractResult<IValidationFinding> {
protected SchemaResult(@NonNull IValidationFinding finding) {
super(finding);
}
@Override
public List<Result> generateResults(@NonNull URI output) throws IOException {
IValidationFinding finding = getFinding();
Result result = new Result();
result.setRuleId(schemaRule.getId());
result.setRuleIndex(BigInteger.valueOf(schemaRule.getIndex()));
result.setGuid(schemaRule.getGuid());
result.setKind(kind(finding).getLabel());
result.setLevel(level(finding.getSeverity()).getLabel());
message(finding, result);
location(finding, result, output);
return CollectionUtil.singletonList(result);
}
}
private final class ConstraintResult
extends AbstractResult<ConstraintValidationFinding> {
protected ConstraintResult(@NonNull ConstraintValidationFinding finding) {
super(finding);
}
@Override
public List<Result> generateResults(@NonNull URI output) throws IOException {
ConstraintValidationFinding finding = getFinding();
List<Result> retval = new LinkedList<>();
Kind kind = kind(finding);
SeverityLevel level = level(finding.getSeverity());
for (IConstraint constraint : finding.getConstraints()) {
assert constraint != null;
ConstraintRuleRecord rule = getRuleRecord(constraint);
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
Result result = new Result();
String id = constraint.getId();
if (id != null) {
result.setRuleId(id);
}
result.setRuleIndex(BigInteger.valueOf(rule.getIndex()));
result.setGuid(rule.getGuid());
result.setKind(kind.getLabel());
result.setLevel(level.getLabel());
message(finding, result);
location(finding, result, output);
retval.add(result);
}
return retval;
}
}
private abstract class AbstractRuleRecord {
private final int index;
@NonNull
private final UUID guid;
private AbstractRuleRecord() {
this.index = ruleIndex.addAndGet(1);
this.guid = ObjectUtils.notNull(UUID.randomUUID());
}
public int getIndex() {
return index;
}
@NonNull
public UUID getGuid() {
return guid;
}
@NonNull
protected abstract ReportingDescriptor generate();
}
private final class SchemaRuleRecord
extends AbstractRuleRecord {
@Override
protected ReportingDescriptor generate() {
ReportingDescriptor retval = new ReportingDescriptor();
retval.setId(getId());
retval.setGuid(getGuid());
return retval;
}
public String getId() {
return "schema-valid";
}
}
private final class ConstraintRuleRecord
extends AbstractRuleRecord {
@NonNull
private final IConstraint constraint;
public ConstraintRuleRecord(@NonNull IConstraint constraint) {
this.constraint = constraint;
}
@NonNull
public IConstraint getConstraint() {
return constraint;
}
@Override
protected ReportingDescriptor generate() {
ReportingDescriptor retval = new ReportingDescriptor();
IConstraint constraint = getConstraint();
UUID guid = getGuid();
String id = constraint.getId();
if (id == null) {
retval.setId(guid.toString());
} else {
retval.setId(id);
}
retval.setGuid(guid);
String formalName = constraint.getFormalName();
if (formalName != null) {
MultiformatMessageString text = new MultiformatMessageString();
text.setText(formalName);
retval.setShortDescription(text);
}
MarkupLine description = constraint.getDescription();
if (description != null) {
MultiformatMessageString text = new MultiformatMessageString();
text.setText(description.toText());
text.setMarkdown(description.toMarkdown());
retval.setFullDescription(text);
}
Set<String> helpUrls = constraint.getPropertyValues(SARIF_HELP_URL_KEY);
if (!helpUrls.isEmpty()) {
retval.setHelpUri(URI.create(helpUrls.stream().findFirst().get()));
}
Set<String> helpText = constraint.getPropertyValues(SARIF_HELP_TEXT_KEY);
Set<String> helpMarkdown = constraint.getPropertyValues(SARIF_HELP_MARKDOWN_KEY);
// if there is help text or markdown, produce a message
if (!helpText.isEmpty() || !helpMarkdown.isEmpty()) {
MultiformatMessageString help = new MultiformatMessageString();
MarkupMultiline markdown = helpMarkdown.stream().map(MarkupMultiline::fromMarkdown).findFirst().orElse(null);
if (markdown != null) {
// markdown is provided
help.setMarkdown(markdown.toMarkdown());
}
String text = helpText.isEmpty()
? ObjectUtils.requireNonNull(markdown).toText() // if text is empty, markdown must be provided
: helpText.stream().findFirst().get(); // use the provided text
help.setText(text);
retval.setHelp(help);
}
return retval;
}
}
private final class ArtifactRecord {
@NonNull
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();
try {
location.setUri(UriUtils.relativize(baseUri, getUri(), true));
} catch (URISyntaxException ex) {
throw new IOException(ex);
}
location.setIndex(BigInteger.valueOf(getIndex()));
return location;
}
}
}