1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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         // 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 }