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.model.IResourceLocation;
10 import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
11 import gov.nist.secauto.metaschema.core.model.constraint.IConstraint;
12 import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
13 import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
14 import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
15 import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
16 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
17 import gov.nist.secauto.metaschema.core.util.IVersionInfo;
18 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
19 import gov.nist.secauto.metaschema.core.util.UriUtils;
20 import gov.nist.secauto.metaschema.databind.IBindingContext;
21 import gov.nist.secauto.metaschema.databind.io.Format;
22 import gov.nist.secauto.metaschema.databind.io.SerializationFeature;
23
24 import org.schemastore.json.sarif.x210.Artifact;
25 import org.schemastore.json.sarif.x210.ArtifactLocation;
26 import org.schemastore.json.sarif.x210.Location;
27 import org.schemastore.json.sarif.x210.LogicalLocation;
28 import org.schemastore.json.sarif.x210.Message;
29 import org.schemastore.json.sarif.x210.MultiformatMessageString;
30 import org.schemastore.json.sarif.x210.PhysicalLocation;
31 import org.schemastore.json.sarif.x210.Region;
32 import org.schemastore.json.sarif.x210.ReportingDescriptor;
33 import org.schemastore.json.sarif.x210.Result;
34 import org.schemastore.json.sarif.x210.Run;
35 import org.schemastore.json.sarif.x210.Sarif;
36 import org.schemastore.json.sarif.x210.Tool;
37 import org.schemastore.json.sarif.x210.ToolComponent;
38
39 import java.io.IOException;
40 import java.math.BigInteger;
41 import java.net.URI;
42 import java.net.URISyntaxException;
43 import java.nio.file.Path;
44 import java.nio.file.StandardOpenOption;
45 import java.util.LinkedHashMap;
46 import java.util.LinkedList;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.UUID;
50 import java.util.concurrent.atomic.AtomicInteger;
51
52 import edu.umd.cs.findbugs.annotations.NonNull;
53 import edu.umd.cs.findbugs.annotations.Nullable;
54
55 public final class SarifValidationHandler {
56 private enum Kind {
57 NOT_APPLICABLE("notApplicable"),
58 PASS("pass"),
59 FAIL("fail"),
60 REVIEW("review"),
61 OPEN("open"),
62 INFORMATIONAL("informational");
63
64 @NonNull
65 private final String label;
66
67 Kind(@NonNull String label) {
68 this.label = label;
69 }
70
71 @NonNull
72 public String getLabel() {
73 return label;
74 }
75 }
76
77 private enum SeverityLevel {
78 NONE("none"),
79 NOTE("note"),
80 WARNING("warning"),
81 ERROR("error");
82
83 @NonNull
84 private final String label;
85
86 SeverityLevel(@NonNull String label) {
87 this.label = label;
88 }
89
90 @NonNull
91 public String getLabel() {
92 return label;
93 }
94 }
95
96 @NonNull
97 private final URI source;
98 @Nullable
99 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
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 }