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