580 lines
18 KiB
Markdown
580 lines
18 KiB
Markdown
# SARIF Export Module Architecture
|
|
|
|
## Overview
|
|
|
|
The **SARIF Export** module provides SARIF 2.1.0 compliant output for StellaOps Scanner findings, enabling integration with GitHub Code Scanning, GitLab SAST, Azure DevOps, and other platforms that consume SARIF.
|
|
|
|
## Current State
|
|
|
|
| Component | Status | Location |
|
|
|-----------|--------|----------|
|
|
| SARIF 2.1.0 Models | **Implemented** | `Scanner.Sarif/Models/SarifModels.cs` |
|
|
| SmartDiff SARIF Generator | **Implemented** | `Scanner.SmartDiff/Output/SarifOutputGenerator.cs` |
|
|
| SmartDiff SARIF Endpoint | **Implemented** | `GET /smart-diff/scans/{scanId}/sarif` |
|
|
| Findings SARIF Mapper | **Implemented** | `Scanner.Sarif/SarifExportService.cs` |
|
|
| SARIF Rule Registry | **Implemented** | `Scanner.Sarif/Rules/SarifRuleRegistry.cs` |
|
|
| Fingerprint Generator | **Implemented** | `Scanner.Sarif/Fingerprints/FingerprintGenerator.cs` |
|
|
| GitHub Upload Client | **Not Implemented** | Proposed |
|
|
|
|
---
|
|
|
|
## Module Location
|
|
|
|
```
|
|
src/Scanner/__Libraries/StellaOps.Scanner.Sarif/
|
|
├── ISarifExportService.cs # Main export interface
|
|
├── SarifExportService.cs # Implementation (DONE)
|
|
├── SarifExportOptions.cs # Configuration (DONE)
|
|
├── FindingInput.cs # Input model (DONE)
|
|
├── Models/
|
|
│ └── SarifModels.cs # Complete SARIF 2.1.0 types (DONE)
|
|
├── Rules/
|
|
│ ├── ISarifRuleRegistry.cs # Rule registry interface (DONE)
|
|
│ └── SarifRuleRegistry.cs # 21 rules implemented (DONE)
|
|
└── Fingerprints/
|
|
├── IFingerprintGenerator.cs # Fingerprint interface (DONE)
|
|
└── FingerprintGenerator.cs # SHA-256 fingerprints (DONE)
|
|
```
|
|
|
|
---
|
|
|
|
## Existing SmartDiff SARIF Implementation
|
|
|
|
The SmartDiff module provides a reference implementation:
|
|
|
|
### SarifModels.cs (Existing)
|
|
|
|
```csharp
|
|
// Already implemented record types
|
|
public sealed record SarifLog(
|
|
string Version,
|
|
string Schema,
|
|
ImmutableArray<SarifRun> Runs);
|
|
|
|
public sealed record SarifRun(
|
|
SarifTool Tool,
|
|
ImmutableArray<SarifResult> Results,
|
|
ImmutableArray<SarifArtifact> Artifacts,
|
|
ImmutableArray<SarifVersionControlDetails> VersionControlProvenance,
|
|
ImmutableDictionary<string, object> Properties);
|
|
|
|
public sealed record SarifResult(
|
|
string RuleId,
|
|
int? RuleIndex,
|
|
SarifLevel Level,
|
|
SarifMessage Message,
|
|
ImmutableArray<SarifLocation> Locations,
|
|
ImmutableDictionary<string, string> Fingerprints,
|
|
ImmutableDictionary<string, string> PartialFingerprints,
|
|
ImmutableDictionary<string, object> Properties);
|
|
```
|
|
|
|
### SarifOutputGenerator.cs (Existing)
|
|
|
|
```csharp
|
|
// Existing generator for SmartDiff findings
|
|
public class SarifOutputGenerator
|
|
{
|
|
public SarifLog Generate(
|
|
IEnumerable<MaterialRiskChangeResult> changes,
|
|
SarifOutputOptions options);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## New Findings SARIF Architecture
|
|
|
|
### ISarifExportService
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Sarif;
|
|
|
|
/// <summary>
|
|
/// Service for exporting scanner findings to SARIF format.
|
|
/// </summary>
|
|
public interface ISarifExportService
|
|
{
|
|
/// <summary>
|
|
/// Export findings to SARIF 2.1.0 format.
|
|
/// </summary>
|
|
/// <param name="findings">Scanner findings to export.</param>
|
|
/// <param name="options">Export options.</param>
|
|
/// <param name="ct">Cancellation token.</param>
|
|
/// <returns>SARIF log document.</returns>
|
|
Task<SarifLog> ExportAsync(
|
|
IEnumerable<Finding> findings,
|
|
SarifExportOptions options,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Export findings to SARIF JSON string.
|
|
/// </summary>
|
|
Task<string> ExportToJsonAsync(
|
|
IEnumerable<Finding> findings,
|
|
SarifExportOptions options,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Export findings to SARIF JSON stream.
|
|
/// </summary>
|
|
Task ExportToStreamAsync(
|
|
IEnumerable<Finding> findings,
|
|
SarifExportOptions options,
|
|
Stream outputStream,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Validate SARIF output against schema.
|
|
/// </summary>
|
|
Task<SarifValidationResult> ValidateAsync(
|
|
SarifLog log,
|
|
CancellationToken ct);
|
|
}
|
|
```
|
|
|
|
### SarifExportOptions
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Sarif;
|
|
|
|
/// <summary>
|
|
/// Options for SARIF export.
|
|
/// </summary>
|
|
public sealed record SarifExportOptions
|
|
{
|
|
/// <summary>Tool name in SARIF output.</summary>
|
|
public string ToolName { get; init; } = "StellaOps Scanner";
|
|
|
|
/// <summary>Tool version.</summary>
|
|
public required string ToolVersion { get; init; }
|
|
|
|
/// <summary>Tool information URI.</summary>
|
|
public string ToolUri { get; init; } = "https://stellaops.io/scanner";
|
|
|
|
/// <summary>Minimum severity to include.</summary>
|
|
public Severity? MinimumSeverity { get; init; }
|
|
|
|
/// <summary>Include reachability evidence in properties.</summary>
|
|
public bool IncludeReachability { get; init; } = true;
|
|
|
|
/// <summary>Include VEX status in properties.</summary>
|
|
public bool IncludeVexStatus { get; init; } = true;
|
|
|
|
/// <summary>Include EPSS scores in properties.</summary>
|
|
public bool IncludeEpss { get; init; } = true;
|
|
|
|
/// <summary>Include KEV status in properties.</summary>
|
|
public bool IncludeKev { get; init; } = true;
|
|
|
|
/// <summary>Include evidence URIs in properties.</summary>
|
|
public bool IncludeEvidenceUris { get; init; } = false;
|
|
|
|
/// <summary>Include attestation reference in run properties.</summary>
|
|
public bool IncludeAttestation { get; init; } = true;
|
|
|
|
/// <summary>Version control provenance.</summary>
|
|
public VersionControlInfo? VersionControl { get; init; }
|
|
|
|
/// <summary>Pretty-print JSON output.</summary>
|
|
public bool IndentedJson { get; init; } = false;
|
|
|
|
/// <summary>Category for GitHub upload (distinguishes multiple tools).</summary>
|
|
public string? Category { get; init; }
|
|
|
|
/// <summary>Base URI for source files.</summary>
|
|
public string? SourceRoot { get; init; }
|
|
}
|
|
|
|
public sealed record VersionControlInfo
|
|
{
|
|
public required string RepositoryUri { get; init; }
|
|
public required string RevisionId { get; init; }
|
|
public string? Branch { get; init; }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rule Registry
|
|
|
|
### ISarifRuleRegistry
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Sarif.Rules;
|
|
|
|
/// <summary>
|
|
/// Registry of SARIF rules for StellaOps findings.
|
|
/// </summary>
|
|
public interface ISarifRuleRegistry
|
|
{
|
|
/// <summary>Get rule by ID.</summary>
|
|
SarifRule? GetRule(string ruleId);
|
|
|
|
/// <summary>Get rule for finding type and severity.</summary>
|
|
SarifRule GetRuleForFinding(FindingType type, Severity severity);
|
|
|
|
/// <summary>Get all registered rules.</summary>
|
|
IReadOnlyList<SarifRule> GetAllRules();
|
|
|
|
/// <summary>Get rules by category.</summary>
|
|
IReadOnlyList<SarifRule> GetRulesByCategory(string category);
|
|
}
|
|
```
|
|
|
|
### Rule Definitions
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Sarif.Rules;
|
|
|
|
public static class VulnerabilityRules
|
|
{
|
|
public static readonly SarifRule Critical = new()
|
|
{
|
|
Id = "STELLA-VULN-001",
|
|
Name = "CriticalVulnerability",
|
|
ShortDescription = "Critical vulnerability detected (CVSS >= 9.0)",
|
|
FullDescription = "A critical severity vulnerability was detected. " +
|
|
"This may be a known exploited vulnerability (KEV) or " +
|
|
"have a CVSS score of 9.0 or higher.",
|
|
HelpUri = "https://stellaops.io/rules/STELLA-VULN-001",
|
|
DefaultLevel = SarifLevel.Error,
|
|
Properties = new Dictionary<string, object>
|
|
{
|
|
["precision"] = "high",
|
|
["problem.severity"] = "error",
|
|
["security-severity"] = "10.0",
|
|
["tags"] = new[] { "security", "vulnerability", "critical" }
|
|
}.ToImmutableDictionary()
|
|
};
|
|
|
|
public static readonly SarifRule High = new()
|
|
{
|
|
Id = "STELLA-VULN-002",
|
|
Name = "HighVulnerability",
|
|
ShortDescription = "High severity vulnerability detected (CVSS 7.0-8.9)",
|
|
FullDescription = "A high severity vulnerability was detected with " +
|
|
"CVSS score between 7.0 and 8.9.",
|
|
HelpUri = "https://stellaops.io/rules/STELLA-VULN-002",
|
|
DefaultLevel = SarifLevel.Error,
|
|
Properties = new Dictionary<string, object>
|
|
{
|
|
["precision"] = "high",
|
|
["problem.severity"] = "error",
|
|
["security-severity"] = "8.0",
|
|
["tags"] = new[] { "security", "vulnerability", "high" }
|
|
}.ToImmutableDictionary()
|
|
};
|
|
|
|
public static readonly SarifRule Medium = new()
|
|
{
|
|
Id = "STELLA-VULN-003",
|
|
Name = "MediumVulnerability",
|
|
ShortDescription = "Medium severity vulnerability detected (CVSS 4.0-6.9)",
|
|
HelpUri = "https://stellaops.io/rules/STELLA-VULN-003",
|
|
DefaultLevel = SarifLevel.Warning,
|
|
Properties = new Dictionary<string, object>
|
|
{
|
|
["precision"] = "high",
|
|
["problem.severity"] = "warning",
|
|
["security-severity"] = "5.5",
|
|
["tags"] = new[] { "security", "vulnerability", "medium" }
|
|
}.ToImmutableDictionary()
|
|
};
|
|
|
|
public static readonly SarifRule Low = new()
|
|
{
|
|
Id = "STELLA-VULN-004",
|
|
Name = "LowVulnerability",
|
|
ShortDescription = "Low severity vulnerability detected (CVSS < 4.0)",
|
|
HelpUri = "https://stellaops.io/rules/STELLA-VULN-004",
|
|
DefaultLevel = SarifLevel.Note,
|
|
Properties = new Dictionary<string, object>
|
|
{
|
|
["precision"] = "high",
|
|
["problem.severity"] = "note",
|
|
["security-severity"] = "2.0",
|
|
["tags"] = new[] { "security", "vulnerability", "low" }
|
|
}.ToImmutableDictionary()
|
|
};
|
|
|
|
// Reachability-enhanced rules
|
|
public static readonly SarifRule RuntimeReachable = new()
|
|
{
|
|
Id = "STELLA-VULN-005",
|
|
Name = "ReachableVulnerability",
|
|
ShortDescription = "Runtime-confirmed reachable vulnerability",
|
|
FullDescription = "A vulnerability with runtime-confirmed reachability. " +
|
|
"The vulnerable code path was observed during execution.",
|
|
HelpUri = "https://stellaops.io/rules/STELLA-VULN-005",
|
|
DefaultLevel = SarifLevel.Error,
|
|
Properties = new Dictionary<string, object>
|
|
{
|
|
["precision"] = "very-high",
|
|
["problem.severity"] = "error",
|
|
["security-severity"] = "9.5",
|
|
["tags"] = new[] { "security", "vulnerability", "reachable", "runtime" }
|
|
}.ToImmutableDictionary()
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Fingerprint Generation
|
|
|
|
### IFingerprintGenerator
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Sarif.Fingerprints;
|
|
|
|
/// <summary>
|
|
/// Generates deterministic fingerprints for SARIF deduplication.
|
|
/// </summary>
|
|
public interface IFingerprintGenerator
|
|
{
|
|
/// <summary>
|
|
/// Generate primary fingerprint for a finding.
|
|
/// </summary>
|
|
string GeneratePrimary(Finding finding, FingerprintStrategy strategy);
|
|
|
|
/// <summary>
|
|
/// Generate partial fingerprints for GitHub fallback.
|
|
/// </summary>
|
|
ImmutableDictionary<string, string> GeneratePartial(
|
|
Finding finding,
|
|
string? sourceContent);
|
|
}
|
|
|
|
public enum FingerprintStrategy
|
|
{
|
|
/// <summary>Hash of ruleId + purl + vulnId + artifactDigest.</summary>
|
|
Standard,
|
|
|
|
/// <summary>Hash including file location for source-level findings.</summary>
|
|
WithLocation,
|
|
|
|
/// <summary>Hash including content hash for maximum stability.</summary>
|
|
ContentBased
|
|
}
|
|
```
|
|
|
|
### Implementation
|
|
|
|
```csharp
|
|
public class FingerprintGenerator : IFingerprintGenerator
|
|
{
|
|
public string GeneratePrimary(Finding finding, FingerprintStrategy strategy)
|
|
{
|
|
var input = strategy switch
|
|
{
|
|
FingerprintStrategy.Standard => string.Join("|",
|
|
finding.RuleId,
|
|
finding.ComponentPurl,
|
|
finding.VulnerabilityId ?? "",
|
|
finding.ArtifactDigest),
|
|
|
|
FingerprintStrategy.WithLocation => string.Join("|",
|
|
finding.RuleId,
|
|
finding.ComponentPurl,
|
|
finding.VulnerabilityId ?? "",
|
|
finding.ArtifactDigest,
|
|
finding.FilePath ?? "",
|
|
finding.LineNumber?.ToString(CultureInfo.InvariantCulture) ?? ""),
|
|
|
|
FingerprintStrategy.ContentBased => string.Join("|",
|
|
finding.RuleId,
|
|
finding.ComponentPurl,
|
|
finding.VulnerabilityId ?? "",
|
|
finding.ContentHash ?? finding.ArtifactDigest),
|
|
|
|
_ => throw new ArgumentOutOfRangeException(nameof(strategy))
|
|
};
|
|
|
|
return ComputeSha256(input);
|
|
}
|
|
|
|
public ImmutableDictionary<string, string> GeneratePartial(
|
|
Finding finding,
|
|
string? sourceContent)
|
|
{
|
|
var partial = new Dictionary<string, string>();
|
|
|
|
// Line hash for GitHub deduplication
|
|
if (!string.IsNullOrEmpty(sourceContent) && finding.LineNumber.HasValue)
|
|
{
|
|
var lines = sourceContent.Split('\n');
|
|
if (finding.LineNumber.Value <= lines.Length)
|
|
{
|
|
var line = lines[finding.LineNumber.Value - 1];
|
|
partial["primaryLocationLineHash"] = ComputeSha256(line.Trim());
|
|
}
|
|
}
|
|
|
|
return partial.ToImmutableDictionary();
|
|
}
|
|
|
|
private static string ComputeSha256(string input)
|
|
{
|
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
|
|
return Convert.ToHexString(bytes).ToLowerInvariant();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Severity Mapping
|
|
|
|
```csharp
|
|
public static class SeverityMapper
|
|
{
|
|
public static SarifLevel MapToSarifLevel(Severity severity, bool isReachable = false)
|
|
{
|
|
// Reachable vulnerabilities are always error level
|
|
if (isReachable && severity >= Severity.Medium)
|
|
return SarifLevel.Error;
|
|
|
|
return severity switch
|
|
{
|
|
Severity.Critical => SarifLevel.Error,
|
|
Severity.High => SarifLevel.Error,
|
|
Severity.Medium => SarifLevel.Warning,
|
|
Severity.Low => SarifLevel.Note,
|
|
Severity.Info => SarifLevel.Note,
|
|
_ => SarifLevel.None
|
|
};
|
|
}
|
|
|
|
public static double MapToSecuritySeverity(double cvssScore)
|
|
{
|
|
// GitHub uses security-severity for ordering
|
|
// Map CVSS 0-10 scale directly
|
|
return Math.Clamp(cvssScore, 0.0, 10.0);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Determinism Requirements
|
|
|
|
Following CLAUDE.md rules:
|
|
|
|
1. **Canonical JSON:** RFC 8785 sorted keys, no nulls
|
|
2. **Stable Rule Ordering:** Rules sorted by ID
|
|
3. **Stable Result Ordering:** Results sorted by (ruleId, location, fingerprint)
|
|
4. **Time Injection:** Use `TimeProvider` for timestamps
|
|
5. **Culture Invariance:** `InvariantCulture` for all string operations
|
|
6. **Immutable Collections:** All outputs use `ImmutableArray`, `ImmutableDictionary`
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Scanner Export Endpoints
|
|
|
|
```csharp
|
|
public static class SarifExportEndpoints
|
|
{
|
|
public static void MapSarifEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/v1/scans/{scanId}/exports")
|
|
.RequireAuthorization("scanner:read");
|
|
|
|
// SARIF export
|
|
group.MapGet("/sarif", ExportSarif)
|
|
.WithName("ExportScanSarif")
|
|
.Produces<string>(StatusCodes.Status200OK, "application/sarif+json");
|
|
|
|
// SARIF with options
|
|
group.MapPost("/sarif", ExportSarifWithOptions)
|
|
.WithName("ExportScanSarifWithOptions")
|
|
.Produces<string>(StatusCodes.Status200OK, "application/sarif+json");
|
|
}
|
|
|
|
private static async Task<IResult> ExportSarif(
|
|
Guid scanId,
|
|
[FromQuery] string? minSeverity,
|
|
[FromQuery] bool pretty = false,
|
|
[FromQuery] bool includeReachability = true,
|
|
ISarifExportService sarifService,
|
|
IFindingsService findingsService,
|
|
CancellationToken ct)
|
|
{
|
|
var findings = await findingsService.GetByScanIdAsync(scanId, ct);
|
|
|
|
var options = new SarifExportOptions
|
|
{
|
|
ToolVersion = GetToolVersion(),
|
|
MinimumSeverity = ParseSeverity(minSeverity),
|
|
IncludeReachability = includeReachability,
|
|
IndentedJson = pretty
|
|
};
|
|
|
|
var json = await sarifService.ExportToJsonAsync(findings, options, ct);
|
|
|
|
return Results.Content(json, "application/sarif+json");
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Integration with GitHub
|
|
|
|
See `src/Integrations/__Plugins/StellaOps.Integrations.Plugin.GitHubApp/` for GitHub connector.
|
|
|
|
New GitHub Code Scanning client extends existing infrastructure:
|
|
|
|
```csharp
|
|
public interface IGitHubCodeScanningClient
|
|
{
|
|
/// <summary>Upload SARIF to GitHub Code Scanning.</summary>
|
|
Task<SarifUploadResult> UploadSarifAsync(
|
|
string owner,
|
|
string repo,
|
|
SarifUploadRequest request,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>Get upload status.</summary>
|
|
Task<SarifUploadStatus> GetUploadStatusAsync(
|
|
string owner,
|
|
string repo,
|
|
string sarifId,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>List code scanning alerts.</summary>
|
|
Task<IReadOnlyList<CodeScanningAlert>> ListAlertsAsync(
|
|
string owner,
|
|
string repo,
|
|
AlertFilter? filter,
|
|
CancellationToken ct);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Targets
|
|
|
|
| Operation | Target P95 | Notes |
|
|
|-----------|-----------|-------|
|
|
| Export 100 findings | < 100ms | In-memory |
|
|
| Export 10,000 findings | < 5s | Streaming |
|
|
| SARIF serialization | < 50ms/MB | RFC 8785 |
|
|
| Schema validation | < 200ms | JSON Schema |
|
|
| Fingerprint generation | < 1ms/finding | SHA-256 |
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Product Advisory](../../product/advisories/09-Jan-2026%20-%20GitHub%20Code%20Scanning%20Integration%20(Revised).md)
|
|
- [SARIF 2.1.0 Specification](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
|
- [GitHub SARIF Support](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning)
|
|
- [Existing SmartDiff SARIF](../../../src/Scanner/__Libraries/StellaOps.Scanner.SmartDiff/Output/)
|
|
|
|
---
|
|
|
|
_Last updated: 09-Jan-2026_
|