001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.modules.sarif;
007
008import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
009import gov.nist.secauto.metaschema.core.model.IResourceLocation;
010import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
011import gov.nist.secauto.metaschema.core.model.constraint.IConstraint;
012import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
013import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
014import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
015import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
016import gov.nist.secauto.metaschema.core.util.CollectionUtil;
017import gov.nist.secauto.metaschema.core.util.IVersionInfo;
018import gov.nist.secauto.metaschema.core.util.ObjectUtils;
019import gov.nist.secauto.metaschema.core.util.UriUtils;
020import gov.nist.secauto.metaschema.databind.IBindingContext;
021import gov.nist.secauto.metaschema.databind.io.Format;
022import gov.nist.secauto.metaschema.databind.io.SerializationFeature;
023
024import org.schemastore.json.sarif.x210.Artifact;
025import org.schemastore.json.sarif.x210.ArtifactLocation;
026import org.schemastore.json.sarif.x210.Location;
027import org.schemastore.json.sarif.x210.LogicalLocation;
028import org.schemastore.json.sarif.x210.Message;
029import org.schemastore.json.sarif.x210.MultiformatMessageString;
030import org.schemastore.json.sarif.x210.PhysicalLocation;
031import org.schemastore.json.sarif.x210.Region;
032import org.schemastore.json.sarif.x210.ReportingDescriptor;
033import org.schemastore.json.sarif.x210.Result;
034import org.schemastore.json.sarif.x210.Run;
035import org.schemastore.json.sarif.x210.Sarif;
036import org.schemastore.json.sarif.x210.Tool;
037import org.schemastore.json.sarif.x210.ToolComponent;
038
039import java.io.IOException;
040import java.math.BigInteger;
041import java.net.URI;
042import java.net.URISyntaxException;
043import java.nio.file.Path;
044import java.nio.file.StandardOpenOption;
045import java.util.LinkedHashMap;
046import java.util.LinkedList;
047import java.util.List;
048import java.util.Map;
049import java.util.UUID;
050import java.util.concurrent.atomic.AtomicInteger;
051
052import edu.umd.cs.findbugs.annotations.NonNull;
053import edu.umd.cs.findbugs.annotations.Nullable;
054
055public final class SarifValidationHandler {
056  private enum Kind {
057    NOT_APPLICABLE("notApplicable"),
058    PASS("pass"),
059    FAIL("fail"),
060    REVIEW("review"),
061    OPEN("open"),
062    INFORMATIONAL("informational");
063
064    @NonNull
065    private final String label;
066
067    Kind(@NonNull String label) {
068      this.label = label;
069    }
070
071    @NonNull
072    public String getLabel() {
073      return label;
074    }
075  }
076
077  private enum SeverityLevel {
078    NONE("none"),
079    NOTE("note"),
080    WARNING("warning"),
081    ERROR("error");
082
083    @NonNull
084    private final String label;
085
086    SeverityLevel(@NonNull String label) {
087      this.label = label;
088    }
089
090    @NonNull
091    public String getLabel() {
092      return label;
093    }
094  }
095
096  @NonNull
097  private final URI source;
098  @Nullable
099  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}