# 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 Runs); public sealed record SarifRun( SarifTool Tool, ImmutableArray Results, ImmutableArray Artifacts, ImmutableArray VersionControlProvenance, ImmutableDictionary Properties); public sealed record SarifResult( string RuleId, int? RuleIndex, SarifLevel Level, SarifMessage Message, ImmutableArray Locations, ImmutableDictionary Fingerprints, ImmutableDictionary PartialFingerprints, ImmutableDictionary Properties); ``` ### SarifOutputGenerator.cs (Existing) ```csharp // Existing generator for SmartDiff findings public class SarifOutputGenerator { public SarifLog Generate( IEnumerable changes, SarifOutputOptions options); } ``` --- ## New Findings SARIF Architecture ### ISarifExportService ```csharp namespace StellaOps.Scanner.Sarif; /// /// Service for exporting scanner findings to SARIF format. /// public interface ISarifExportService { /// /// Export findings to SARIF 2.1.0 format. /// /// Scanner findings to export. /// Export options. /// Cancellation token. /// SARIF log document. Task ExportAsync( IEnumerable findings, SarifExportOptions options, CancellationToken ct); /// /// Export findings to SARIF JSON string. /// Task ExportToJsonAsync( IEnumerable findings, SarifExportOptions options, CancellationToken ct); /// /// Export findings to SARIF JSON stream. /// Task ExportToStreamAsync( IEnumerable findings, SarifExportOptions options, Stream outputStream, CancellationToken ct); /// /// Validate SARIF output against schema. /// Task ValidateAsync( SarifLog log, CancellationToken ct); } ``` ### SarifExportOptions ```csharp namespace StellaOps.Scanner.Sarif; /// /// Options for SARIF export. /// public sealed record SarifExportOptions { /// Tool name in SARIF output. public string ToolName { get; init; } = "StellaOps Scanner"; /// Tool version. public required string ToolVersion { get; init; } /// Tool information URI. public string ToolUri { get; init; } = "https://stellaops.io/scanner"; /// Minimum severity to include. public Severity? MinimumSeverity { get; init; } /// Include reachability evidence in properties. public bool IncludeReachability { get; init; } = true; /// Include VEX status in properties. public bool IncludeVexStatus { get; init; } = true; /// Include EPSS scores in properties. public bool IncludeEpss { get; init; } = true; /// Include KEV status in properties. public bool IncludeKev { get; init; } = true; /// Include evidence URIs in properties. public bool IncludeEvidenceUris { get; init; } = false; /// Include attestation reference in run properties. public bool IncludeAttestation { get; init; } = true; /// Version control provenance. public VersionControlInfo? VersionControl { get; init; } /// Pretty-print JSON output. public bool IndentedJson { get; init; } = false; /// Category for GitHub upload (distinguishes multiple tools). public string? Category { get; init; } /// Base URI for source files. 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; /// /// Registry of SARIF rules for StellaOps findings. /// public interface ISarifRuleRegistry { /// Get rule by ID. SarifRule? GetRule(string ruleId); /// Get rule for finding type and severity. SarifRule GetRuleForFinding(FindingType type, Severity severity); /// Get all registered rules. IReadOnlyList GetAllRules(); /// Get rules by category. IReadOnlyList 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 { ["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 { ["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 { ["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 { ["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 { ["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; /// /// Generates deterministic fingerprints for SARIF deduplication. /// public interface IFingerprintGenerator { /// /// Generate primary fingerprint for a finding. /// string GeneratePrimary(Finding finding, FingerprintStrategy strategy); /// /// Generate partial fingerprints for GitHub fallback. /// ImmutableDictionary GeneratePartial( Finding finding, string? sourceContent); } public enum FingerprintStrategy { /// Hash of ruleId + purl + vulnId + artifactDigest. Standard, /// Hash including file location for source-level findings. WithLocation, /// Hash including content hash for maximum stability. 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 GeneratePartial( Finding finding, string? sourceContent) { var partial = new Dictionary(); // 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(StatusCodes.Status200OK, "application/sarif+json"); // SARIF with options group.MapPost("/sarif", ExportSarifWithOptions) .WithName("ExportScanSarifWithOptions") .Produces(StatusCodes.Status200OK, "application/sarif+json"); } private static async Task 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 { /// Upload SARIF to GitHub Code Scanning. Task UploadSarifAsync( string owner, string repo, SarifUploadRequest request, CancellationToken ct); /// Get upload status. Task GetUploadStatusAsync( string owner, string repo, string sarifId, CancellationToken ct); /// List code scanning alerts. Task> 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_