1
2
3
4
5
6 package dev.metaschema.modules.sarif;
7
8 import org.schemastore.json.sarif.x210.Artifact;
9 import org.schemastore.json.sarif.x210.ArtifactLocation;
10 import org.schemastore.json.sarif.x210.Location;
11 import org.schemastore.json.sarif.x210.LogicalLocation;
12 import org.schemastore.json.sarif.x210.Message;
13 import org.schemastore.json.sarif.x210.MultiformatMessageString;
14 import org.schemastore.json.sarif.x210.PhysicalLocation;
15 import org.schemastore.json.sarif.x210.Region;
16 import org.schemastore.json.sarif.x210.ReportingDescriptor;
17 import org.schemastore.json.sarif.x210.Result;
18 import org.schemastore.json.sarif.x210.Run;
19 import org.schemastore.json.sarif.x210.Sarif;
20 import org.schemastore.json.sarif.x210.SarifModule;
21 import org.schemastore.json.sarif.x210.Tool;
22 import org.schemastore.json.sarif.x210.ToolComponent;
23
24 import java.io.IOException;
25 import java.io.StringWriter;
26 import java.math.BigInteger;
27 import java.net.URI;
28 import java.net.URISyntaxException;
29 import java.nio.file.Path;
30 import java.nio.file.StandardOpenOption;
31 import java.util.Collection;
32 import java.util.LinkedHashMap;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.UUID;
38 import java.util.concurrent.atomic.AtomicInteger;
39
40 import dev.metaschema.core.datatype.markup.MarkupLine;
41 import dev.metaschema.core.datatype.markup.MarkupMultiline;
42 import dev.metaschema.core.model.IAttributable;
43 import dev.metaschema.core.model.IResourceLocation;
44 import dev.metaschema.core.model.MetaschemaException;
45 import dev.metaschema.core.model.constraint.ConstraintValidationFinding;
46 import dev.metaschema.core.model.constraint.IConstraint;
47 import dev.metaschema.core.model.constraint.IConstraint.Level;
48 import dev.metaschema.core.model.validation.IValidationFinding;
49 import dev.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
50 import dev.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
51 import dev.metaschema.core.util.CollectionUtil;
52 import dev.metaschema.core.util.IVersionInfo;
53 import dev.metaschema.core.util.ObjectUtils;
54 import dev.metaschema.core.util.UriUtils;
55 import dev.metaschema.databind.IBindingContext;
56 import dev.metaschema.databind.io.Format;
57 import dev.metaschema.databind.io.SerializationFeature;
58 import edu.umd.cs.findbugs.annotations.NonNull;
59 import edu.umd.cs.findbugs.annotations.Nullable;
60
61
62
63
64
65 @SuppressWarnings("PMD.CouplingBetweenObjects")
66 public final class SarifValidationHandler {
67 private enum Kind {
68 NOT_APPLICABLE("notApplicable"),
69 PASS("pass"),
70 FAIL("fail"),
71 REVIEW("review"),
72 OPEN("open"),
73 INFORMATIONAL("informational");
74
75 @NonNull
76 private final String label;
77
78 Kind(@NonNull String label) {
79 this.label = label;
80 }
81
82 @NonNull
83 public String getLabel() {
84 return label;
85 }
86 }
87
88 private enum SeverityLevel {
89 NONE("none"),
90 NOTE("note"),
91 WARNING("warning"),
92 ERROR("error");
93
94 @NonNull
95 private final String label;
96
97 SeverityLevel(@NonNull String label) {
98 this.label = label;
99 }
100
101 @NonNull
102 public String getLabel() {
103 return label;
104 }
105 }
106
107 @NonNull
108 static final String SARIF_NS = "https://docs.oasis-open.org/sarif/sarif/v2.1.0";
109
110
111
112
113 @NonNull
114 public static final IAttributable.Key SARIF_HELP_URL_KEY
115 = IAttributable.key("help-url", SARIF_NS);
116
117
118
119 @NonNull
120 public static final IAttributable.Key SARIF_HELP_TEXT_KEY
121 = IAttributable.key("help-text", SARIF_NS);
122
123
124
125
126 @NonNull
127 public static final IAttributable.Key SARIF_HELP_MARKDOWN_KEY
128 = IAttributable.key("help-markdown", SARIF_NS);
129
130 @NonNull
131 private final URI source;
132 @Nullable
133 private final IVersionInfo toolVersion;
134 private final AtomicInteger artifactIndex = new AtomicInteger(-1);
135 private final AtomicInteger ruleIndex = new AtomicInteger(-1);
136
137 @SuppressWarnings("PMD.UseConcurrentHashMap")
138 @NonNull
139 private final Map<URI, ArtifactRecord> artifacts = new LinkedHashMap<>();
140 @NonNull
141 private final List<AbstractRuleRecord> rules = new LinkedList<>();
142 @SuppressWarnings("PMD.UseConcurrentHashMap")
143 @NonNull
144 private final Map<IConstraint, ConstraintRuleRecord> constraintRules = new LinkedHashMap<>();
145 @NonNull
146 private final List<IResult> results = new LinkedList<>();
147 @NonNull
148 private final SchemaRuleRecord schemaRule = new SchemaRuleRecord();
149 private boolean schemaValid = true;
150
151
152
153
154
155
156
157
158
159
160 public SarifValidationHandler(
161 @NonNull URI source,
162 @Nullable IVersionInfo toolVersion) {
163 if (!source.isAbsolute()) {
164 throw new IllegalArgumentException(String.format("The source URI '%s' is not absolute.", source.toASCIIString()));
165 }
166
167 this.source = source;
168 this.toolVersion = toolVersion;
169 }
170
171 @NonNull
172 private URI getSource() {
173 return source;
174 }
175
176 private IVersionInfo getToolVersion() {
177 return toolVersion;
178 }
179
180
181
182
183
184
185
186 public void addFindings(@NonNull Collection<? extends IValidationFinding> findings) {
187 for (IValidationFinding finding : findings) {
188 assert finding != null;
189 addFinding(finding);
190 }
191 }
192
193
194
195
196
197
198
199 public void addFinding(@NonNull IValidationFinding finding) {
200 if (finding instanceof JsonValidationFinding) {
201 addJsonValidationFinding((JsonValidationFinding) finding);
202 } else if (finding instanceof XmlValidationFinding) {
203 addXmlValidationFinding((XmlValidationFinding) finding);
204 } else if (finding instanceof ConstraintValidationFinding) {
205 addConstraintValidationFinding((ConstraintValidationFinding) finding);
206 } else {
207 throw new IllegalStateException();
208 }
209 }
210
211 private ConstraintRuleRecord getRuleRecord(@NonNull IConstraint constraint) {
212 ConstraintRuleRecord retval = constraintRules.get(constraint);
213 if (retval == null) {
214 retval = new ConstraintRuleRecord(constraint);
215 constraintRules.put(constraint, retval);
216 rules.add(retval);
217 }
218 return retval;
219 }
220
221 private ArtifactRecord getArtifactRecord(@NonNull URI artifactUri) {
222 ArtifactRecord retval = artifacts.get(artifactUri);
223 if (retval == null) {
224 retval = new ArtifactRecord(artifactUri);
225 artifacts.put(artifactUri, retval);
226 }
227 return retval;
228 }
229
230 private void addJsonValidationFinding(@NonNull JsonValidationFinding finding) {
231 results.add(new SchemaResult(finding));
232 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
233 schemaValid = false;
234 }
235 }
236
237 private void addXmlValidationFinding(@NonNull XmlValidationFinding finding) {
238 results.add(new SchemaResult(finding));
239 if (schemaValid && IValidationFinding.Kind.FAIL.equals(finding.getKind())) {
240 schemaValid = false;
241 }
242 }
243
244 private void addConstraintValidationFinding(@NonNull ConstraintValidationFinding finding) {
245 results.add(new ConstraintResult(finding));
246 }
247
248
249
250
251
252
253
254
255
256
257 @NonNull
258 private Sarif generateSarif(@NonNull URI outputUri) throws IOException {
259 Sarif sarif = new Sarif();
260 sarif.setVersion("2.1.0");
261
262 Run run = new Run();
263 sarif.addRun(run);
264
265 Artifact artifact = new Artifact();
266 artifact.setLocation(getArtifactRecord(getSource()).generateArtifactLocation(outputUri));
267 run.addArtifact(artifact);
268
269 for (IResult result : results) {
270 result.generateResults(outputUri).forEach(run::addResult);
271 }
272
273 IVersionInfo toolVersion = getToolVersion();
274 if (!rules.isEmpty() || toolVersion != null) {
275 Tool tool = new Tool();
276 ToolComponent driver = new ToolComponent();
277
278 if (toolVersion != null) {
279 driver.setName(toolVersion.getName());
280 driver.setVersion(toolVersion.getVersion());
281 }
282
283 for (AbstractRuleRecord rule : rules) {
284 driver.addRule(rule.generate());
285 }
286
287 tool.setDriver(driver);
288 run.setTool(tool);
289 }
290
291 return sarif;
292 }
293
294
295
296
297
298
299
300
301
302
303
304 @NonNull
305 public String writeToString(@NonNull IBindingContext bindingContext) throws IOException {
306 registerSarifMetaschemaModule(bindingContext);
307 try (StringWriter writer = new StringWriter()) {
308 bindingContext.newSerializer(Format.JSON, Sarif.class)
309 .disableFeature(SerializationFeature.SERIALIZE_ROOT)
310 .serialize(generateSarif(getSource()), writer);
311 return ObjectUtils.notNull(writer.toString());
312 }
313 }
314
315
316
317
318
319
320
321
322
323
324
325
326 public void write(
327 @NonNull Path outputFile,
328 @NonNull IBindingContext bindingContext) throws IOException {
329
330 URI output = ObjectUtils.notNull(outputFile.toUri());
331 Sarif sarif = generateSarif(output);
332
333 registerSarifMetaschemaModule(bindingContext);
334 bindingContext.newSerializer(Format.JSON, Sarif.class)
335 .disableFeature(SerializationFeature.SERIALIZE_ROOT)
336 .serialize(
337 sarif,
338 outputFile,
339 StandardOpenOption.CREATE,
340 StandardOpenOption.WRITE,
341 StandardOpenOption.TRUNCATE_EXISTING);
342 }
343
344 private static void registerSarifMetaschemaModule(@NonNull IBindingContext bindingContext) {
345 try {
346 bindingContext.registerModule(SarifModule.class);
347 } catch (MetaschemaException ex) {
348 throw new IllegalStateException("Unable to register the builtin SARIF module.", ex);
349 }
350 }
351
352 private interface IResult {
353 @NonNull
354 IValidationFinding getFinding();
355
356 @NonNull
357 List<Result> generateResults(@NonNull URI output) throws IOException;
358 }
359
360 private abstract class AbstractResult<T extends IValidationFinding> implements IResult {
361 @NonNull
362 private final T finding;
363
364 protected AbstractResult(@NonNull T finding) {
365 this.finding = finding;
366 }
367
368 @Override
369 public T getFinding() {
370 return finding;
371 }
372
373 @NonNull
374 protected Kind kind(@NonNull IValidationFinding finding) {
375 IValidationFinding.Kind kind = finding.getKind();
376
377 Kind retval;
378 switch (kind) {
379 case FAIL:
380 retval = Kind.FAIL;
381 break;
382 case INFORMATIONAL:
383 retval = Kind.INFORMATIONAL;
384 break;
385 case NOT_APPLICABLE:
386 retval = Kind.NOT_APPLICABLE;
387 break;
388 case PASS:
389 retval = Kind.PASS;
390 break;
391 default:
392 throw new IllegalArgumentException(String.format("Invalid finding kind '%s'.", kind));
393 }
394 return retval;
395 }
396
397 @NonNull
398 protected SeverityLevel level(@NonNull Level severity) {
399 SeverityLevel retval;
400 switch (severity) {
401 case CRITICAL:
402 case ERROR:
403 retval = SeverityLevel.ERROR;
404 break;
405 case INFORMATIONAL:
406 case DEBUG:
407 retval = SeverityLevel.NOTE;
408 break;
409 case WARNING:
410 retval = SeverityLevel.WARNING;
411 break;
412 case NONE:
413 retval = SeverityLevel.NONE;
414 break;
415 default:
416 throw new IllegalArgumentException(String.format("Invalid severity '%s'.", severity));
417 }
418 return retval;
419 }
420
421 protected void message(@NonNull IValidationFinding finding, @NonNull Result result) {
422 String message = finding.getMessage();
423 if (message == null) {
424 message = "";
425 }
426
427 Message msg = new Message();
428 msg.setText(message);
429 result.setMessage(msg);
430 }
431
432 protected void location(@NonNull IValidationFinding finding, @NonNull Result result, @NonNull URI base)
433 throws IOException {
434 IResourceLocation location = finding.getLocation();
435 if (location != null) {
436
437 Region region = new Region();
438
439 if (location.getLine() > -1) {
440 region.setStartLine(BigInteger.valueOf(location.getLine()));
441 region.setEndLine(BigInteger.valueOf(location.getLine()));
442 }
443 if (location.getColumn() > -1) {
444 region.setStartColumn(BigInteger.valueOf(location.getColumn() + 1));
445 region.setEndColumn(BigInteger.valueOf(location.getColumn() + 1));
446 }
447 if (location.getByteOffset() > -1) {
448 region.setByteOffset(BigInteger.valueOf(location.getByteOffset()));
449 region.setByteLength(BigInteger.ZERO);
450 }
451 if (location.getCharOffset() > -1) {
452 region.setCharOffset(BigInteger.valueOf(location.getCharOffset()));
453 region.setCharLength(BigInteger.ZERO);
454 }
455
456 PhysicalLocation physical = new PhysicalLocation();
457
458 URI documentUri = finding.getDocumentUri();
459 if (documentUri != null) {
460 physical.setArtifactLocation(getArtifactRecord(documentUri).generateArtifactLocation(base));
461 }
462 physical.setRegion(region);
463
464 LogicalLocation logical = new LogicalLocation();
465
466 logical.setDecoratedName(finding.getPath());
467
468 Location loc = new Location();
469 loc.setPhysicalLocation(physical);
470 loc.addLogicalLocation(logical);
471 result.addLocation(loc);
472 }
473 }
474 }
475
476 private final class SchemaResult
477 extends AbstractResult<IValidationFinding> {
478
479 protected SchemaResult(@NonNull IValidationFinding finding) {
480 super(finding);
481 }
482
483 @Override
484 public List<Result> generateResults(@NonNull URI output) throws IOException {
485 IValidationFinding finding = getFinding();
486
487 Result result = new Result();
488
489 result.setRuleId(schemaRule.getId());
490 result.setRuleIndex(BigInteger.valueOf(schemaRule.getIndex()));
491 result.setGuid(schemaRule.getGuid());
492
493 result.setKind(kind(finding).getLabel());
494 result.setLevel(level(finding.getSeverity()).getLabel());
495 message(finding, result);
496 location(finding, result, output);
497
498 return CollectionUtil.singletonList(result);
499 }
500 }
501
502 private final class ConstraintResult
503 extends AbstractResult<ConstraintValidationFinding> {
504
505 protected ConstraintResult(@NonNull ConstraintValidationFinding finding) {
506 super(finding);
507 }
508
509 @Override
510 public List<Result> generateResults(@NonNull URI output) throws IOException {
511 ConstraintValidationFinding finding = getFinding();
512
513 List<Result> retval = new LinkedList<>();
514
515 Kind kind = kind(finding);
516 SeverityLevel level = level(finding.getSeverity());
517
518 for (IConstraint constraint : finding.getConstraints()) {
519 assert constraint != null;
520 ConstraintRuleRecord rule = getRuleRecord(constraint);
521
522 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
523 Result result = new Result();
524
525 String id = constraint.getId();
526 if (id != null) {
527 result.setRuleId(id);
528 }
529 result.setRuleIndex(BigInteger.valueOf(rule.getIndex()));
530 result.setGuid(rule.getGuid());
531 result.setKind(kind.getLabel());
532 result.setLevel(level.getLabel());
533 message(finding, result);
534 location(finding, result, output);
535
536 retval.add(result);
537 }
538 return retval;
539 }
540 }
541
542 private abstract class AbstractRuleRecord {
543 private final int index;
544 @NonNull
545 private final UUID guid;
546
547 private AbstractRuleRecord() {
548 this.index = ruleIndex.addAndGet(1);
549 this.guid = ObjectUtils.notNull(UUID.randomUUID());
550 }
551
552 public int getIndex() {
553 return index;
554 }
555
556 @NonNull
557 public UUID getGuid() {
558 return guid;
559 }
560
561 @NonNull
562 protected abstract ReportingDescriptor generate();
563 }
564
565 private final class SchemaRuleRecord
566 extends AbstractRuleRecord {
567
568 @Override
569 protected ReportingDescriptor generate() {
570 ReportingDescriptor retval = new ReportingDescriptor();
571 retval.setId(getId());
572 retval.setGuid(getGuid());
573 return retval;
574 }
575
576 public String getId() {
577 return "schema-valid";
578 }
579 }
580
581 private final class ConstraintRuleRecord
582 extends AbstractRuleRecord {
583 @NonNull
584 private final IConstraint constraint;
585
586 public ConstraintRuleRecord(@NonNull IConstraint constraint) {
587 this.constraint = constraint;
588 }
589
590 @NonNull
591 public IConstraint getConstraint() {
592 return constraint;
593 }
594
595 @Override
596 protected ReportingDescriptor generate() {
597 ReportingDescriptor retval = new ReportingDescriptor();
598 IConstraint constraint = getConstraint();
599
600 UUID guid = getGuid();
601
602 String id = constraint.getId();
603 if (id == null) {
604 retval.setId(guid.toString());
605 } else {
606 retval.setId(id);
607 }
608 retval.setGuid(guid);
609 String formalName = constraint.getFormalName();
610 if (formalName != null) {
611 MultiformatMessageString text = new MultiformatMessageString();
612 text.setText(formalName);
613 retval.setShortDescription(text);
614 }
615 MarkupLine description = constraint.getDescription();
616 if (description != null) {
617 MultiformatMessageString text = new MultiformatMessageString();
618 text.setText(description.toText());
619 text.setMarkdown(description.toMarkdown());
620 retval.setFullDescription(text);
621 }
622
623 Set<String> helpUrls = constraint.getPropertyValues(SARIF_HELP_URL_KEY);
624 if (!helpUrls.isEmpty()) {
625 retval.setHelpUri(URI.create(helpUrls.stream().findFirst().get()));
626 }
627
628 Set<String> helpText = constraint.getPropertyValues(SARIF_HELP_TEXT_KEY);
629 Set<String> helpMarkdown = constraint.getPropertyValues(SARIF_HELP_MARKDOWN_KEY);
630
631 if (!helpText.isEmpty() || !helpMarkdown.isEmpty()) {
632 MultiformatMessageString help = new MultiformatMessageString();
633
634 MarkupMultiline markdown = helpMarkdown.stream().map(MarkupMultiline::fromMarkdown).findFirst().orElse(null);
635 if (markdown != null) {
636
637 help.setMarkdown(markdown.toMarkdown());
638 }
639
640 String text = helpText.isEmpty()
641 ? ObjectUtils.requireNonNull(markdown).toText()
642 : helpText.stream().findFirst().get();
643 help.setText(text);
644
645 retval.setHelp(help);
646 }
647
648 return retval;
649 }
650
651 }
652
653 private final class ArtifactRecord {
654 @NonNull
655 private final URI uri;
656 private final int index;
657
658 public ArtifactRecord(@NonNull URI uri) {
659 this.uri = uri;
660 this.index = artifactIndex.addAndGet(1);
661 }
662
663 @NonNull
664 public URI getUri() {
665 return uri;
666 }
667
668 public int getIndex() {
669 return index;
670 }
671
672 public ArtifactLocation generateArtifactLocation(@NonNull URI baseUri) throws IOException {
673 ArtifactLocation location = new ArtifactLocation();
674
675 try {
676 location.setUri(UriUtils.relativize(baseUri, getUri(), true));
677 } catch (URISyntaxException ex) {
678 throw new IOException(ex);
679 }
680
681 location.setIndex(BigInteger.valueOf(getIndex()));
682 return location;
683 }
684 }
685 }