001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.modules.sarif; 007 008import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine; 009import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; 010import gov.nist.secauto.metaschema.core.model.IAttributable; 011import gov.nist.secauto.metaschema.core.model.IResourceLocation; 012import gov.nist.secauto.metaschema.core.model.MetaschemaException; 013import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding; 014import gov.nist.secauto.metaschema.core.model.constraint.IConstraint; 015import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; 016import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding; 017import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding; 018import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding; 019import gov.nist.secauto.metaschema.core.util.CollectionUtil; 020import gov.nist.secauto.metaschema.core.util.IVersionInfo; 021import gov.nist.secauto.metaschema.core.util.ObjectUtils; 022import gov.nist.secauto.metaschema.core.util.UriUtils; 023import gov.nist.secauto.metaschema.databind.IBindingContext; 024import gov.nist.secauto.metaschema.databind.io.Format; 025import gov.nist.secauto.metaschema.databind.io.SerializationFeature; 026 027import org.schemastore.json.sarif.x210.Artifact; 028import org.schemastore.json.sarif.x210.ArtifactLocation; 029import org.schemastore.json.sarif.x210.Location; 030import org.schemastore.json.sarif.x210.LogicalLocation; 031import org.schemastore.json.sarif.x210.Message; 032import org.schemastore.json.sarif.x210.MultiformatMessageString; 033import org.schemastore.json.sarif.x210.PhysicalLocation; 034import org.schemastore.json.sarif.x210.Region; 035import org.schemastore.json.sarif.x210.ReportingDescriptor; 036import org.schemastore.json.sarif.x210.Result; 037import org.schemastore.json.sarif.x210.Run; 038import org.schemastore.json.sarif.x210.Sarif; 039import org.schemastore.json.sarif.x210.SarifModule; 040import org.schemastore.json.sarif.x210.Tool; 041import org.schemastore.json.sarif.x210.ToolComponent; 042 043import java.io.IOException; 044import java.io.StringWriter; 045import java.math.BigInteger; 046import java.net.URI; 047import java.net.URISyntaxException; 048import java.nio.file.Path; 049import java.nio.file.StandardOpenOption; 050import java.util.Collection; 051import java.util.LinkedHashMap; 052import java.util.LinkedList; 053import java.util.List; 054import java.util.Map; 055import java.util.Set; 056import java.util.UUID; 057import java.util.concurrent.atomic.AtomicInteger; 058 059import edu.umd.cs.findbugs.annotations.NonNull; 060import edu.umd.cs.findbugs.annotations.Nullable; 061 062/** 063 * Supports building a Static Analysis Results Interchange Format (SARIF) 064 * document based on a set of validation findings. 065 */ 066@SuppressWarnings("PMD.CouplingBetweenObjects") 067public final class SarifValidationHandler { 068 private enum Kind { 069 NOT_APPLICABLE("notApplicable"), 070 PASS("pass"), 071 FAIL("fail"), 072 REVIEW("review"), 073 OPEN("open"), 074 INFORMATIONAL("informational"); 075 076 @NonNull 077 private final String label; 078 079 Kind(@NonNull String label) { 080 this.label = label; 081 } 082 083 @NonNull 084 public String getLabel() { 085 return label; 086 } 087 } 088 089 private enum SeverityLevel { 090 NONE("none"), 091 NOTE("note"), 092 WARNING("warning"), 093 ERROR("error"); 094 095 @NonNull 096 private final String label; 097 098 SeverityLevel(@NonNull String label) { 099 this.label = label; 100 } 101 102 @NonNull 103 public String getLabel() { 104 return label; 105 } 106 } 107 108 @NonNull 109 static final String SARIF_NS = "https://docs.oasis-open.org/sarif/sarif/v2.1.0"; 110 @NonNull 111 public static final IAttributable.Key SARIF_HELP_URL_KEY 112 = IAttributable.key("help-url", SARIF_NS); 113 @NonNull 114 public static final IAttributable.Key SARIF_HELP_TEXT_KEY 115 = IAttributable.key("help-text", SARIF_NS); 116 @NonNull 117 public static final IAttributable.Key SARIF_HELP_MARKDOWN_KEY 118 = IAttributable.key("help-markdown", SARIF_NS); 119 120 @NonNull 121 private final URI source; 122 @Nullable 123 private final IVersionInfo toolVersion; 124 private final AtomicInteger artifactIndex = new AtomicInteger(-1); 125 private final AtomicInteger ruleIndex = new AtomicInteger(-1); 126 127 @SuppressWarnings("PMD.UseConcurrentHashMap") 128 @NonNull 129 private final Map<URI, ArtifactRecord> artifacts = new LinkedHashMap<>(); 130 @NonNull 131 private final List<AbstractRuleRecord> rules = new LinkedList<>(); 132 @SuppressWarnings("PMD.UseConcurrentHashMap") 133 @NonNull 134 private final Map<IConstraint, ConstraintRuleRecord> constraintRules = new LinkedHashMap<>(); 135 @NonNull 136 private final List<IResult> results = new LinkedList<>(); 137 @NonNull 138 private final SchemaRuleRecord schemaRule = new SchemaRuleRecord(); 139 private boolean schemaValid = true; 140 141 /** 142 * Construct a new validation handler. 143 * 144 * @param source 145 * the URI of the content that was validated 146 * @param toolVersion 147 * the version information for the tool producing the validation 148 * results 149 */ 150 public SarifValidationHandler( 151 @NonNull URI source, 152 @Nullable IVersionInfo toolVersion) { 153 if (!source.isAbsolute()) { 154 throw new IllegalArgumentException(String.format("The source URI '%s' is not absolute.", source.toASCIIString())); 155 } 156 157 this.source = source; 158 this.toolVersion = toolVersion; 159 } 160 161 @NonNull 162 private URI getSource() { 163 return source; 164 } 165 166 private IVersionInfo getToolVersion() { 167 return toolVersion; 168 } 169 170 /** 171 * Register a collection of validation finding. 172 * 173 * @param findings 174 * the findings to register 175 */ 176 public void addFindings(@NonNull Collection<? extends IValidationFinding> findings) { 177 for (IValidationFinding finding : findings) { 178 assert finding != null; 179 addFinding(finding); 180 } 181 } 182 183 /** 184 * Register a validation finding. 185 * 186 * @param finding 187 * the finding to register 188 */ 189 public void addFinding(@NonNull IValidationFinding finding) { 190 if (finding instanceof JsonValidationFinding) { 191 addJsonValidationFinding((JsonValidationFinding) finding); 192 } else if (finding instanceof XmlValidationFinding) { 193 addXmlValidationFinding((XmlValidationFinding) finding); 194 } else if (finding instanceof ConstraintValidationFinding) { 195 addConstraintValidationFinding((ConstraintValidationFinding) finding); 196 } else { 197 throw new IllegalStateException(); 198 } 199 } 200 201 private ConstraintRuleRecord getRuleRecord(@NonNull IConstraint constraint) { 202 ConstraintRuleRecord retval = constraintRules.get(constraint); 203 if (retval == null) { 204 retval = new ConstraintRuleRecord(constraint); 205 constraintRules.put(constraint, retval); 206 rules.add(retval); 207 } 208 return retval; 209 } 210 211 private ArtifactRecord getArtifactRecord(@NonNull URI artifactUri) { 212 ArtifactRecord retval = artifacts.get(artifactUri); 213 if (retval == null) { 214 retval = new ArtifactRecord(artifactUri); 215 artifacts.put(artifactUri, retval); 216 } 217 return retval; 218 } 219 220 private void addJsonValidationFinding(@NonNull JsonValidationFinding finding) { 221 results.add(new SchemaResult(finding)); 222 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) { 223 schemaValid = false; 224 } 225 } 226 227 private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) { 228 results.add(new SchemaResult(finding)); 229 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) { 230 schemaValid = false; 231 } 232 } 233 234 private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) { 235 results.add(new ConstraintResult(finding)); 236 } 237 238 /** 239 * Generate a SARIF document based on the collected findings. 240 * 241 * @param outputUri 242 * the URI to use as the base for relative paths in the SARIF document 243 * @return the generated SARIF document 244 * @throws IOException 245 * if an error occurred while generating the SARIF document 246 */ 247 @NonNull 248 private Sarif generateSarif(@NonNull URI outputUri) throws IOException { 249 Sarif sarif = new Sarif(); 250 sarif.setVersion("2.1.0"); 251 252 Run run = new Run(); 253 sarif.addRun(run); 254 255 Artifact artifact = new Artifact(); 256 artifact.setLocation(getArtifactRecord(getSource()).generateArtifactLocation(outputUri)); 257 run.addArtifact(artifact); 258 259 for (IResult result : results) { 260 result.generateResults(outputUri).forEach(run::addResult); 261 } 262 263 IVersionInfo toolVersion = getToolVersion(); 264 if (!rules.isEmpty() || toolVersion != null) { 265 Tool tool = new Tool(); 266 ToolComponent driver = new ToolComponent(); 267 268 if (toolVersion != null) { 269 driver.setName(toolVersion.getName()); 270 driver.setVersion(toolVersion.getVersion()); 271 } 272 273 for (AbstractRuleRecord rule : rules) { 274 driver.addRule(rule.generate()); 275 } 276 277 tool.setDriver(driver); 278 run.setTool(tool); 279 } 280 281 return sarif; 282 } 283 284 /** 285 * Write the collection of findings to a string in SARIF format. 286 * 287 * @param bindingContext 288 * the context used to access Metaschema module information based on 289 * Java class bindings 290 * @return the SARIF document as a string 291 * @throws IOException 292 * if an error occurred while generating the SARIF document 293 */ 294 @NonNull 295 public String writeToString(@NonNull IBindingContext bindingContext) throws IOException { 296 registerSarifMetaschemaModule(bindingContext); 297 try (StringWriter writer = new StringWriter()) { 298 bindingContext.newSerializer(Format.JSON, Sarif.class) 299 .disableFeature(SerializationFeature.SERIALIZE_ROOT) 300 .serialize(generateSarif(getSource()), writer); 301 return ObjectUtils.notNull(writer.toString()); 302 } 303 } 304 305 /** 306 * Write the collection of findings to the provided output file. 307 * 308 * @param outputFile 309 * the path to the output file to write to 310 * @param bindingContext 311 * the context used to access Metaschema module information based on 312 * Java class bindings 313 * @throws IOException 314 * if an error occurred while writing the SARIF file 315 */ 316 public void write( 317 @NonNull Path outputFile, 318 @NonNull IBindingContext bindingContext) throws IOException { 319 320 URI output = ObjectUtils.notNull(outputFile.toUri()); 321 Sarif sarif = generateSarif(output); 322 323 registerSarifMetaschemaModule(bindingContext); 324 bindingContext.newSerializer(Format.JSON, Sarif.class) 325 .disableFeature(SerializationFeature.SERIALIZE_ROOT) 326 .serialize( 327 sarif, 328 outputFile, 329 StandardOpenOption.CREATE, 330 StandardOpenOption.WRITE, 331 StandardOpenOption.TRUNCATE_EXISTING); 332 } 333 334 private static void registerSarifMetaschemaModule(@NonNull IBindingContext bindingContext) { 335 try { 336 bindingContext.registerModule(SarifModule.class); 337 } catch (MetaschemaException ex) { 338 throw new IllegalStateException("Unable to register the builtin SARIF module.", ex); 339 } 340 } 341 342 private interface IResult { 343 @NonNull 344 IValidationFinding getFinding(); 345 346 @NonNull 347 List<Result> generateResults(@NonNull URI output) throws IOException; 348 } 349 350 private abstract class AbstractResult<T extends IValidationFinding> implements IResult { 351 @NonNull 352 private final T finding; 353 354 protected AbstractResult(@NonNull T finding) { 355 this.finding = finding; 356 } 357 358 @Override 359 public T getFinding() { 360 return finding; 361 } 362 363 @NonNull 364 protected Kind kind(@NonNull IValidationFinding finding) { 365 IValidationFinding.Kind kind = finding.getKind(); 366 367 Kind retval; 368 switch (kind) { 369 case FAIL: 370 retval = Kind.FAIL; 371 break; 372 case INFORMATIONAL: 373 retval = Kind.INFORMATIONAL; 374 break; 375 case NOT_APPLICABLE: 376 retval = Kind.NOT_APPLICABLE; 377 break; 378 case PASS: 379 retval = Kind.PASS; 380 break; 381 default: 382 throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind)); 383 } 384 return retval; 385 } 386 387 @NonNull 388 protected SeverityLevel level(@NonNull Level severity) { 389 SeverityLevel retval; 390 switch (severity) { 391 case CRITICAL: 392 case ERROR: 393 retval = SeverityLevel.ERROR; 394 break; 395 case INFORMATIONAL: 396 case DEBUG: 397 retval = SeverityLevel.NOTE; 398 break; 399 case WARNING: 400 retval = SeverityLevel.WARNING; 401 break; 402 case NONE: 403 retval = SeverityLevel.NONE; 404 break; 405 default: 406 throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity)); 407 } 408 return retval; 409 } 410 411 protected void message(@NonNull IValidationFinding finding, @NonNull Result result) { 412 String message = finding.getMessage(); 413 if (message == null) { 414 message = ""; 415 } 416 417 Message msg = new Message(); 418 msg.setText(message); 419 result.setMessage(msg); 420 } 421 422 protected void location(@NonNull IValidationFinding finding, @NonNull Result result, @NonNull URI base) 423 throws IOException { 424 IResourceLocation location = finding.getLocation(); 425 if (location != null) { 426 // region 427 Region region = new Region(); 428 429 if (location.getLine() > -1) { 430 region.setStartLine(BigInteger.valueOf(location.getLine())); 431 region.setEndLine(BigInteger.valueOf(location.getLine())); 432 } 433 if (location.getColumn() > -1) { 434 region.setStartColumn(BigInteger.valueOf(location.getColumn() + 1)); 435 region.setEndColumn(BigInteger.valueOf(location.getColumn() + 1)); 436 } 437 if (location.getByteOffset() > -1) { 438 region.setByteOffset(BigInteger.valueOf(location.getByteOffset())); 439 region.setByteLength(BigInteger.ZERO); 440 } 441 if (location.getCharOffset() > -1) { 442 region.setCharOffset(BigInteger.valueOf(location.getCharOffset())); 443 region.setCharLength(BigInteger.ZERO); 444 } 445 446 PhysicalLocation physical = new PhysicalLocation(); 447 448 URI documentUri = finding.getDocumentUri(); 449 if (documentUri != null) { 450 physical.setArtifactLocation(getArtifactRecord(documentUri).generateArtifactLocation(base)); 451 } 452 physical.setRegion(region); 453 454 LogicalLocation logical = new LogicalLocation(); 455 456 logical.setDecoratedName(finding.getPath()); 457 458 Location loc = new Location(); 459 loc.setPhysicalLocation(physical); 460 loc.addLogicalLocation(logical); 461 result.addLocation(loc); 462 } 463 } 464 } 465 466 private final class SchemaResult 467 extends AbstractResult<IValidationFinding> { 468 469 protected SchemaResult(@NonNull IValidationFinding finding) { 470 super(finding); 471 } 472 473 @Override 474 public List<Result> generateResults(@NonNull URI output) throws IOException { 475 IValidationFinding finding = getFinding(); 476 477 Result result = new Result(); 478 479 result.setRuleId(schemaRule.getId()); 480 result.setRuleIndex(BigInteger.valueOf(schemaRule.getIndex())); 481 result.setGuid(schemaRule.getGuid()); 482 483 result.setKind(kind(finding).getLabel()); 484 result.setLevel(level(finding.getSeverity()).getLabel()); 485 message(finding, result); 486 location(finding, result, output); 487 488 return CollectionUtil.singletonList(result); 489 } 490 } 491 492 private final class ConstraintResult 493 extends AbstractResult<ConstraintValidationFinding> { 494 495 protected ConstraintResult(@NonNull ConstraintValidationFinding finding) { 496 super(finding); 497 } 498 499 @Override 500 public List<Result> generateResults(@NonNull URI output) throws IOException { 501 ConstraintValidationFinding finding = getFinding(); 502 503 List<Result> retval = new LinkedList<>(); 504 505 Kind kind = kind(finding); 506 SeverityLevel level = level(finding.getSeverity()); 507 508 for (IConstraint constraint : finding.getConstraints()) { 509 assert constraint != null; 510 ConstraintRuleRecord rule = getRuleRecord(constraint); 511 512 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 513 Result result = new Result(); 514 515 String id = constraint.getId(); 516 if (id != null) { 517 result.setRuleId(id); 518 } 519 result.setRuleIndex(BigInteger.valueOf(rule.getIndex())); 520 result.setGuid(rule.getGuid()); 521 result.setKind(kind.getLabel()); 522 result.setLevel(level.getLabel()); 523 message(finding, result); 524 location(finding, result, output); 525 526 retval.add(result); 527 } 528 return retval; 529 } 530 } 531 532 private abstract class AbstractRuleRecord { 533 private final int index; 534 @NonNull 535 private final UUID guid; 536 537 private AbstractRuleRecord() { 538 this.index = ruleIndex.addAndGet(1); 539 this.guid = ObjectUtils.notNull(UUID.randomUUID()); 540 } 541 542 public int getIndex() { 543 return index; 544 } 545 546 @NonNull 547 public UUID getGuid() { 548 return guid; 549 } 550 551 @NonNull 552 protected abstract ReportingDescriptor generate(); 553 } 554 555 private final class SchemaRuleRecord 556 extends AbstractRuleRecord { 557 558 @Override 559 protected ReportingDescriptor generate() { 560 ReportingDescriptor retval = new ReportingDescriptor(); 561 retval.setId(getId()); 562 retval.setGuid(getGuid()); 563 return retval; 564 } 565 566 public String getId() { 567 return "schema-valid"; 568 } 569 } 570 571 private final class ConstraintRuleRecord 572 extends AbstractRuleRecord { 573 @NonNull 574 private final IConstraint constraint; 575 576 public ConstraintRuleRecord(@NonNull IConstraint constraint) { 577 this.constraint = constraint; 578 } 579 580 @NonNull 581 public IConstraint getConstraint() { 582 return constraint; 583 } 584 585 @Override 586 protected ReportingDescriptor generate() { 587 ReportingDescriptor retval = new ReportingDescriptor(); 588 IConstraint constraint = getConstraint(); 589 590 UUID guid = getGuid(); 591 592 String id = constraint.getId(); 593 if (id == null) { 594 retval.setId(guid.toString()); 595 } else { 596 retval.setId(id); 597 } 598 retval.setGuid(guid); 599 String formalName = constraint.getFormalName(); 600 if (formalName != null) { 601 MultiformatMessageString text = new MultiformatMessageString(); 602 text.setText(formalName); 603 retval.setShortDescription(text); 604 } 605 MarkupLine description = constraint.getDescription(); 606 if (description != null) { 607 MultiformatMessageString text = new MultiformatMessageString(); 608 text.setText(description.toText()); 609 text.setMarkdown(description.toMarkdown()); 610 retval.setFullDescription(text); 611 } 612 613 Set<String> helpUrls = constraint.getPropertyValues(SARIF_HELP_URL_KEY); 614 if (!helpUrls.isEmpty()) { 615 retval.setHelpUri(URI.create(helpUrls.stream().findFirst().get())); 616 } 617 618 Set<String> helpText = constraint.getPropertyValues(SARIF_HELP_TEXT_KEY); 619 Set<String> helpMarkdown = constraint.getPropertyValues(SARIF_HELP_MARKDOWN_KEY); 620 // if there is help text or markdown, produce a message 621 if (!helpText.isEmpty() || !helpMarkdown.isEmpty()) { 622 MultiformatMessageString help = new MultiformatMessageString(); 623 624 MarkupMultiline markdown = helpMarkdown.stream().map(MarkupMultiline::fromMarkdown).findFirst().orElse(null); 625 if (markdown != null) { 626 // markdown is provided 627 help.setMarkdown(markdown.toMarkdown()); 628 } 629 630 String text = helpText.isEmpty() 631 ? ObjectUtils.requireNonNull(markdown).toText() // if text is empty, markdown must be provided 632 : helpText.stream().findFirst().get(); // use the provided text 633 help.setText(text); 634 635 retval.setHelp(help); 636 } 637 638 return retval; 639 } 640 641 } 642 643 private final class ArtifactRecord { 644 @NonNull 645 private final URI uri; 646 private final int index; 647 648 public ArtifactRecord(@NonNull URI uri) { 649 this.uri = uri; 650 this.index = artifactIndex.addAndGet(1); 651 } 652 653 @NonNull 654 public URI getUri() { 655 return uri; 656 } 657 658 public int getIndex() { 659 return index; 660 } 661 662 public ArtifactLocation generateArtifactLocation(@NonNull URI baseUri) throws IOException { 663 ArtifactLocation location = new ArtifactLocation(); 664 665 try { 666 location.setUri(UriUtils.relativize(baseUri, getUri(), true)); 667 } catch (URISyntaxException ex) { 668 throw new IOException(ex); 669 } 670 671 location.setIndex(BigInteger.valueOf(getIndex())); 672 return location; 673 } 674 } 675}