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