1
2
3
4
5
6 package gov.nist.secauto.metaschema.modules.sarif;
7
8 import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
9 import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
10 import gov.nist.secauto.metaschema.core.model.IAttributable;
11 import gov.nist.secauto.metaschema.core.model.IResourceLocation;
12 import gov.nist.secauto.metaschema.core.model.MetaschemaException;
13 import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
14 import gov.nist.secauto.metaschema.core.model.constraint.IConstraint;
15 import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
16 import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
17 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
18 import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
19 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
20 import gov.nist.secauto.metaschema.core.util.IVersionInfo;
21 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
22 import gov.nist.secauto.metaschema.core.util.UriUtils;
23 import gov.nist.secauto.metaschema.databind.IBindingContext;
24 import gov.nist.secauto.metaschema.databind.io.Format;
25 import gov.nist.secauto.metaschema.databind.io.SerializationFeature;
26
27 import org.schemastore.json.sarif.x210.Artifact;
28 import org.schemastore.json.sarif.x210.ArtifactLocation;
29 import org.schemastore.json.sarif.x210.Location;
30 import org.schemastore.json.sarif.x210.LogicalLocation;
31 import org.schemastore.json.sarif.x210.Message;
32 import org.schemastore.json.sarif.x210.MultiformatMessageString;
33 import org.schemastore.json.sarif.x210.PhysicalLocation;
34 import org.schemastore.json.sarif.x210.Region;
35 import org.schemastore.json.sarif.x210.ReportingDescriptor;
36 import org.schemastore.json.sarif.x210.Result;
37 import org.schemastore.json.sarif.x210.Run;
38 import org.schemastore.json.sarif.x210.Sarif;
39 import org.schemastore.json.sarif.x210.SarifModule;
40 import org.schemastore.json.sarif.x210.Tool;
41 import org.schemastore.json.sarif.x210.ToolComponent;
42
43 import java.io.IOException;
44 import java.io.StringWriter;
45 import java.math.BigInteger;
46 import java.net.URI;
47 import java.net.URISyntaxException;
48 import java.nio.file.Path;
49 import java.nio.file.StandardOpenOption;
50 import java.util.Collection;
51 import java.util.LinkedHashMap;
52 import java.util.LinkedList;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 import java.util.UUID;
57 import java.util.concurrent.atomic.AtomicInteger;
58
59 import edu.umd.cs.findbugs.annotations.NonNull;
60 import edu.umd.cs.findbugs.annotations.Nullable;
61
62
63
64
65
66 @SuppressWarnings("PMD.CouplingBetweenObjects")
67 public final class SarifValidationHandler {
68 private enum Kind {
69 NOT_APPLICABLE("notApplicable"),
70 PASS("pass"),
71 FAIL("fail"),
72 REVIEW("review"),
73 OPEN("open"),
74 INFORMATIONAL("informational");
75
76 @NonNull
77 private final String label;
78
79 Kind(@NonNull String label) {
80 this.label = label;
81 }
82
83 @NonNull
84 public String getLabel() {
85 return label;
86 }
87 }
88
89 private enum SeverityLevel {
90 NONE("none"),
91 NOTE("note"),
92 WARNING("warning"),
93 ERROR("error");
94
95 @NonNull
96 private final String label;
97
98 SeverityLevel(@NonNull String label) {
99 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
143
144
145
146
147
148
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
172
173
174
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
185
186
187
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
240
241
242
243
244
245
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
286
287
288
289
290
291
292
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
307
308
309
310
311
312
313
314
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
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
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
627 help.setMarkdown(markdown.toMarkdown());
628 }
629
630 String text = helpText.isEmpty()
631 ? ObjectUtils.requireNonNull(markdown).toText()
632 : helpText.stream().findFirst().get();
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 }