save progress
This commit is contained in:
579
docs/modules/sarif-export/architecture.md
Normal file
579
docs/modules/sarif-export/architecture.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# 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_
|
||||
Reference in New Issue
Block a user