Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 122 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,44 @@ Writes a [SonarQube generic issue report](https://docs.sonarsource.com/sonarqube
slnx-validator MySolution.slnx --sonarqube-report-file sonar-issues.json --continue-on-error
```

### `--sarif-report-file <file>`

[SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) (Static Analysis Results Interchange Format) is an open OASIS standard for static analysis tool output. It enables interoperability between analysis tools and result viewers, so the same report can be consumed by GitHub Code Scanning, Azure DevOps, Visual Studio, Visual Studio Code, and other tools without any conversion.

**Benefits of SARIF:**
- Native integration with [GitHub Code Scanning](https://docs.github.com/en/code-security/code-scanning) — issues appear as alerts in the **Security → Code Scanning** tab
- Supported via extensions for Visual Studio, Visual Studio Code, and Azure DevOps
- Rich result format: rule metadata, severity, file paths, and line numbers in a single file
- Widely adopted standard — see [SARIF tutorials](https://github.com/microsoft/sarif-tutorials) and the [SARIF web viewer](https://sarifweb.azurewebsites.net/)

**Usage:**

```powershell
slnx-validator MySolution.slnx --sarif-report-file results.sarif --continue-on-error
```

Severity mapping from `RuleSeverity` to SARIF levels (see also the [GitHub Code Scanning integration example](#github-code-scanning-integration-example)):

| Severity | SARIF level |
|----------|-------------|
| `BLOCKER`, `CRITICAL`, `MAJOR` | `error` |
| `MINOR` | `warning` |
| `INFO` | `note` |

Severity overrides (via `--minor`, `--info`, `--ignore`, etc.) are reflected in the SARIF output. See [Severity override flags](#severity-override-flags) for details.

**Viewers and reporting:**
- [GitHub Code Scanning](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github) — upload via `github/codeql-action/upload-sarif@v3` (see [example below](#github-code-scanning-integration-example))
- Visual Studio — requires the [Microsoft SARIF Viewer extension](https://marketplace.visualstudio.com/items?itemName=WDGIS.MicrosoftSarifViewer2022)
- Visual Studio Code — requires the [SARIF Viewer extension](https://marketplace.visualstudio.com/items?itemName=MS-SarifVSCode.sarif-viewer)
- Azure DevOps — requires the [SARIF SAST Scans Tab extension](https://marketplace.visualstudio.com/items?itemName=sariftools.scans)
- [SARIF web viewer](https://sarifweb.azurewebsites.net/) — online viewer for quick inspection

Further reading: [SARIF tutorials](https://github.com/microsoft/sarif-tutorials) · [Why SARIF?](https://github.com/microsoft/sarif-tutorials/blob/main/docs/1-Introduction.md#why-sarif) · [SonarSource SARIF overview](https://www.sonarsource.com/resources/library/sarif/)

### `--continue-on-error`

Always exits with code `0`, even when validation errors are found. Useful in CI pipelines where SonarQube handles the failure decision. Default: `false`.
Always exits with code `0`, even when validation errors are found. Useful in CI pipelines where SonarQube or GitHub Code Scanning handles the failure decision. Default: `false`.

### `--required-files`

Expand Down Expand Up @@ -120,16 +155,16 @@ slnx-validator MySolution.slnx --required-files "appsettings.json;docs/"

### Severity override flags

Override the severity of specific validation codes, or suppress them entirely. This controls both the exit code behaviour and the severity written to the SonarQube JSON report.
Override the severity of specific validation codes, or suppress them entirely. This controls the exit code behaviour, the console output label, and the severity written to SonarQube and SARIF reports.

| Flag | Severity | Causes exit code `1`? |
|------|----------|-----------------------|
| `--blocker <codes>` | `BLOCKER` | ✅ yes |
| `--critical <codes>` | `CRITICAL` | ✅ yes |
| `--major <codes>` | `MAJOR` | ✅ yes (default for all codes) |
| `--minor <codes>` | `MINOR` | ❌ no — shown with `(warning)` label |
| `--info <codes>` | `INFO` | ❌ no — shown with `(info)` label |
| `--ignore <codes>` | *(suppressed)* | ❌ no — not shown at all, not in SonarQube report |
| Flag | Severity | Causes exit code `1`? | SARIF level |
|------|----------|-----------------------|-------------|
| `--blocker <codes>` | `BLOCKER` | ✅ yes | `error` |
| `--critical <codes>` | `CRITICAL` | ✅ yes | `error` |
| `--major <codes>` | `MAJOR` | ✅ yes (default for all codes) | `error` |
| `--minor <codes>` | `MINOR` | ❌ no — shown with `(warning)` label | `warning` |
| `--info <codes>` | `INFO` | ❌ no — shown with `(info)` label | `note` |
| `--ignore <codes>` | *(suppressed)* | ❌ no — not shown at all, not in SonarQube or SARIF report | *(omitted)* |

Each flag accepts a **comma-separated list of codes** or the **wildcard `*`** to match all codes:

Expand All @@ -156,9 +191,9 @@ When `*` is combined with specific code flags, **specific codes always win over
slnx-validator MySolution.slnx --info * --major SLNX011
```

**Effect on SonarQube report**
**Effect on reports**

Severity overrides are reflected in the generated rule definition in the JSON report:
Severity overrides are reflected in the generated rule definition in both SonarQube and SARIF reports:

```json
{
Expand All @@ -168,7 +203,7 @@ Severity overrides are reflected in the generated rule definition in the JSON re
}
```

Codes set to `--ignore` are excluded from both the `rules` and `issues` arrays entirely.
Codes set to `--ignore` are excluded from both the `rules` and `issues`/`results` arrays entirely.

## SonarQube integration example

Expand Down Expand Up @@ -220,6 +255,80 @@ sonar.sources=${{ parameters.slnPath }}
sonar.xml.file.suffixes=.xml,.xsd,.xsl,.slnx
```

## GitHub Code Scanning integration example

Use `--sarif-report-file` to generate a SARIF 2.1.0 file and upload it to GitHub Code Scanning:

```powershell
slnx-validator MySolution.slnx --sarif-report-file results.sarif --continue-on-error
```

```yaml
- name: Validate .slnx files
run: slnx-validator MySolution.slnx --sarif-report-file results.sarif --continue-on-error

- name: Upload SARIF to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
```

This uploads the validation results to the **Security → Code Scanning** tab of your repository. Issues appear as alerts with rule descriptions, file locations, and links back to the relevant lines.

Example SARIF output:

```json
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "slnx-validator",
"informationUri": "https://github.com/304NotModified/SLNX-validator",
"rules": [
{
"id": "SLNX011",
"shortDescription": {
"text": "Referenced file not found"
},
"fullDescription": {
"text": "A file referenced in a <File Path=\"...\"> element does not exist on disk."
},
"defaultConfiguration": {
"level": "error"
}
}
]
}
},
"results": [
{
"ruleId": "SLNX011",
"level": "error",
"message": {
"text": "File not found: docs\\CONTRIBUTING.md"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "MySolution.slnx"
},
"region": {
"startLine": 4
}
}
}
]
}
]
}
]
}
```

## Example output

### All valid ✅
Expand Down
7 changes: 7 additions & 0 deletions src/SLNX-validator.Core/Reporting/Rule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;

public sealed record Rule(
string Id,
string Name,
string Description,
RuleSeverity DefaultSeverity);
60 changes: 60 additions & 0 deletions src/SLNX-validator.Core/Reporting/RuleProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;

namespace JulianVerdurmen.SlnxValidator.Core.Reporting;

public static class RuleProvider
{
private static (ValidationErrorCode Key, Rule Rule) Create(
ValidationErrorCode code, string name, string description,
RuleSeverity severity = RuleSeverity.MAJOR) =>
(code, new Rule(code.ToCode(), name, description, severity));

private static readonly Dictionary<ValidationErrorCode, Rule> Rules =
new (ValidationErrorCode Key, Rule Rule)[]
{
Create(ValidationErrorCode.FileNotFound,
"SLNX file not found",
"The specified .slnx file does not exist."),

Create(ValidationErrorCode.InvalidExtension,
"Invalid solution file extension",
"The input file does not have a .slnx extension.",
RuleSeverity.MINOR),

Create(ValidationErrorCode.NotATextFile,
"SLNX file is not a text file",
"The file is binary and cannot be parsed as XML."),

Create(ValidationErrorCode.InvalidXml,
"Invalid XML",
"The .slnx file is not valid XML."),

Create(ValidationErrorCode.ReferencedFileNotFound,
"Referenced file not found",
"A file referenced in a <File Path=\"...\"> element does not exist on disk."),

Create(ValidationErrorCode.InvalidWildcardUsage,
"Invalid wildcard usage",
"A <File Path=\"...\"> element contains a wildcard pattern, which is not supported.",
RuleSeverity.MINOR),

Create(ValidationErrorCode.XsdViolation,
"XSD schema violation",
"The XML structure violates the .slnx schema."),

Create(ValidationErrorCode.RequiredFileDoesntExistOnSystem,
"Required file does not exist on the system",
"A file required by '--required-files' does not exist on the file system."),

Create(ValidationErrorCode.RequiredFileNotReferencedInSolution,
"Required file not referenced in solution",
"A file required by '--required-files' exists on the file system but is not referenced as a <File> element in the solution."),
}.ToDictionary(e => e.Key, e => e.Rule);

public static Rule Get(ValidationErrorCode code)
{
if (Rules.TryGetValue(code, out var rule))
return rule;
throw new ArgumentOutOfRangeException(nameof(code), code, null);
}
}
16 changes: 16 additions & 0 deletions src/SLNX-validator.Core/Reporting/RuleSeverity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace JulianVerdurmen.SlnxValidator.Core.Reporting;

/// <summary>Universal severity level used across the entire validator pipeline.</summary>
public enum RuleSeverity
{
/// <summary>SARIF: <c>error</c> — causes exit code 1.</summary>
BLOCKER,
/// <summary>SARIF: <c>error</c> — causes exit code 1.</summary>
CRITICAL,
/// <summary>SARIF: <c>error</c> — causes exit code 1. Default severity for most rules.</summary>
MAJOR,
/// <summary>SARIF: <c>warning</c> — does not cause exit code 1.</summary>
MINOR,
/// <summary>SARIF: <c>note</c> — does not cause exit code 1.</summary>
INFO
}
13 changes: 13 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/ISarifReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using JulianVerdurmen.SlnxValidator.Core.Reporting;
using JulianVerdurmen.SlnxValidator.Core.ValidationResults;

namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

public interface ISarifReporter
{
Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, string outputPath,
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);

Task WriteReportAsync(IReadOnlyList<FileValidationResult> results, Stream outputStream,
IReadOnlyDictionary<ValidationErrorCode, RuleSeverity?>? severityOverrides = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifArtifactLocation
{
public required string Uri { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifDefaultConfiguration
{
public required string Level { get; init; }
}
6 changes: 6 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/SarifLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifLocation
{
public required SarifPhysicalLocation PhysicalLocation { get; init; }
}
13 changes: 13 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/SarifLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifLog
{
[JsonPropertyName("$schema")]
public required string Schema { get; init; }

public required string Version { get; init; }

public required List<SarifRun> Runs { get; init; }
}
6 changes: 6 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/SarifMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifMessage
{
public required string Text { get; init; }
}
11 changes: 11 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/SarifPhysicalLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifPhysicalLocation
{
public required SarifArtifactLocation ArtifactLocation { get; init; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public SarifRegion? Region { get; init; }
}
6 changes: 6 additions & 0 deletions src/SLNX-validator.Core/SarifReporting/SarifRegion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JulianVerdurmen.SlnxValidator.Core.SarifReporting;

internal sealed record SarifRegion
{
public required int StartLine { get; init; }
}
Loading
Loading