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