18 KiB
18 KiB
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)
// 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)
// Existing generator for SmartDiff findings
public class SarifOutputGenerator
{
public SarifLog Generate(
IEnumerable<MaterialRiskChangeResult> changes,
SarifOutputOptions options);
}
New Findings SARIF Architecture
ISarifExportService
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
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
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
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
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
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
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:
- Canonical JSON: RFC 8785 sorted keys, no nulls
- Stable Rule Ordering: Rules sorted by ID
- Stable Result Ordering: Results sorted by (ruleId, location, fingerprint)
- Time Injection: Use
TimeProviderfor timestamps - Culture Invariance:
InvariantCulturefor all string operations - Immutable Collections: All outputs use
ImmutableArray,ImmutableDictionary
API Endpoints
Scanner Export Endpoints
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:
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
Last updated: 09-Jan-2026