save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View 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_