Add inline DSSE provenance documentation and Mongo schema
- Introduced a new document outlining the inline DSSE provenance for SBOM, VEX, scan, and derived events. - Defined the Mongo schema for event patches, including key fields for provenance and trust verification. - Documented the write path for ingesting provenance metadata and backfilling historical events. - Created CI/CD snippets for uploading DSSE attestations and generating provenance metadata. - Established Mongo indexes for efficient provenance queries and provided query recipes for various use cases. - Outlined policy gates for managing VEX decisions based on provenance verification. - Included UI nudges for displaying provenance information and implementation tasks for future enhancements. --- Implement reachability lattice and scoring model - Developed a comprehensive document detailing the reachability lattice and scoring model. - Defined core types for reachability states, evidence, and mitigations with corresponding C# models. - Established a scoring policy with base score contributions from various evidence classes. - Mapped reachability states to VEX gates and provided a clear overview of evidence sources. - Documented the event graph schema for persisting reachability data in MongoDB. - Outlined the integration of runtime probes for evidence collection and defined a roadmap for future tasks. --- Introduce uncertainty states and entropy scoring - Created a draft document for tracking uncertainty states and their impact on risk scoring. - Defined core uncertainty states with associated entropy values and evidence requirements. - Established a schema for storing uncertainty states alongside findings. - Documented the risk score calculation incorporating uncertainty and its effect on final risk assessments. - Provided policy guidelines for handling uncertainty in decision-making processes. - Outlined UI guidelines for displaying uncertainty information and suggested remediation actions. --- Add Ruby package inventory management - Implemented Ruby package inventory management with corresponding data models and storage mechanisms. - Created C# records for Ruby package inventory, artifacts, provenance, and runtime details. - Developed a repository for managing Ruby package inventory documents in MongoDB. - Implemented a service for storing and retrieving Ruby package inventories. - Added unit tests for the Ruby package inventory store to ensure functionality and data integrity.
This commit is contained in:
@@ -6888,14 +6888,23 @@ internal static class CommandHandlers
|
||||
logger.LogInformation("Resolving Ruby packages for scan {ScanId}.", identifier);
|
||||
activity?.SetTag("stellaops.cli.scan_id", identifier);
|
||||
|
||||
var packages = await client.GetRubyPackagesAsync(identifier, cancellationToken).ConfigureAwait(false);
|
||||
var report = RubyResolveReport.Create(identifier, packages);
|
||||
var inventory = await client.GetRubyPackagesAsync(identifier, cancellationToken).ConfigureAwait(false);
|
||||
if (inventory is null)
|
||||
{
|
||||
outcome = "empty";
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.MarkupLine("[yellow]Ruby package inventory is not available for scan {0}.[/]", Markup.Escape(identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
var report = RubyResolveReport.Create(inventory);
|
||||
|
||||
if (!report.HasPackages)
|
||||
{
|
||||
outcome = "empty";
|
||||
Environment.ExitCode = 0;
|
||||
AnsiConsole.MarkupLine("[yellow]No Ruby packages found for scan {0}.[/]", Markup.Escape(identifier));
|
||||
var displayScanId = string.IsNullOrWhiteSpace(report.ScanId) ? identifier : report.ScanId;
|
||||
AnsiConsole.MarkupLine("[yellow]No Ruby packages found for scan {0}.[/]", Markup.Escape(displayScanId));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7225,6 +7234,12 @@ internal static class CommandHandlers
|
||||
[JsonPropertyName("scanId")]
|
||||
public string ScanId { get; }
|
||||
|
||||
[JsonPropertyName("imageDigest")]
|
||||
public string ImageDigest { get; }
|
||||
|
||||
[JsonPropertyName("generatedAt")]
|
||||
public DateTimeOffset GeneratedAt { get; }
|
||||
|
||||
[JsonPropertyName("groups")]
|
||||
public IReadOnlyList<RubyResolveGroup> Groups { get; }
|
||||
|
||||
@@ -7234,15 +7249,17 @@ internal static class CommandHandlers
|
||||
[JsonIgnore]
|
||||
public int TotalPackages => Groups.Sum(static group => group.Packages.Count);
|
||||
|
||||
private RubyResolveReport(string scanId, IReadOnlyList<RubyResolveGroup> groups)
|
||||
private RubyResolveReport(string scanId, string imageDigest, DateTimeOffset generatedAt, IReadOnlyList<RubyResolveGroup> groups)
|
||||
{
|
||||
ScanId = scanId;
|
||||
ImageDigest = imageDigest;
|
||||
GeneratedAt = generatedAt;
|
||||
Groups = groups;
|
||||
}
|
||||
|
||||
public static RubyResolveReport Create(string scanId, IReadOnlyList<RubyPackageArtifactModel>? packages)
|
||||
public static RubyResolveReport Create(RubyPackageInventoryModel inventory)
|
||||
{
|
||||
var resolved = (packages ?? Array.Empty<RubyPackageArtifactModel>())
|
||||
var resolved = (inventory.Packages ?? Array.Empty<RubyPackageArtifactModel>())
|
||||
.Select(RubyResolvePackage.FromModel)
|
||||
.ToArray();
|
||||
|
||||
@@ -7272,7 +7289,9 @@ internal static class CommandHandlers
|
||||
.ToArray()))
|
||||
.ToArray();
|
||||
|
||||
return new RubyResolveReport(scanId, grouped);
|
||||
var normalizedScanId = inventory.ScanId ?? string.Empty;
|
||||
var normalizedDigest = inventory.ImageDigest ?? string.Empty;
|
||||
return new RubyResolveReport(normalizedScanId, normalizedDigest, inventory.GeneratedAt, grouped);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +892,7 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RubyPackageArtifactModel>> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
|
||||
public async Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
EnsureBackendConfigured();
|
||||
|
||||
@@ -907,7 +907,7 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return Array.Empty<RubyPackageArtifactModel>();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -916,11 +916,25 @@ internal sealed class BackendOperationsClient : IBackendOperationsClient
|
||||
throw new InvalidOperationException(failure);
|
||||
}
|
||||
|
||||
var packages = await response.Content
|
||||
.ReadFromJsonAsync<IReadOnlyList<RubyPackageArtifactModel>>(SerializerOptions, cancellationToken)
|
||||
var inventory = await response.Content
|
||||
.ReadFromJsonAsync<RubyPackageInventoryModel>(SerializerOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return packages ?? Array.Empty<RubyPackageArtifactModel>();
|
||||
if (inventory is null)
|
||||
{
|
||||
throw new InvalidOperationException("Ruby package response payload was empty.");
|
||||
}
|
||||
|
||||
var normalizedScanId = string.IsNullOrWhiteSpace(inventory.ScanId) ? scanId : inventory.ScanId;
|
||||
var normalizedDigest = inventory.ImageDigest ?? string.Empty;
|
||||
var packages = inventory.Packages ?? Array.Empty<RubyPackageArtifactModel>();
|
||||
|
||||
return inventory with
|
||||
{
|
||||
ScanId = normalizedScanId,
|
||||
ImageDigest = normalizedDigest,
|
||||
Packages = packages
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(
|
||||
|
||||
@@ -49,7 +49,7 @@ internal interface IBackendOperationsClient
|
||||
|
||||
Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
Task<IReadOnlyList<RubyPackageArtifactModel>> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken);
|
||||
Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken);
|
||||
|
||||
Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -26,3 +27,8 @@ internal sealed record RubyPackageRuntime(
|
||||
[property: JsonPropertyName("files")] IReadOnlyList<string>? Files,
|
||||
[property: JsonPropertyName("reasons")] IReadOnlyList<string>? Reasons);
|
||||
|
||||
internal sealed record RubyPackageInventoryModel(
|
||||
[property: JsonPropertyName("scanId")] string ScanId,
|
||||
[property: JsonPropertyName("imageDigest")] string ImageDigest,
|
||||
[property: JsonPropertyName("generatedAt")] DateTimeOffset GeneratedAt,
|
||||
[property: JsonPropertyName("packages")] IReadOnlyList<RubyPackageArtifactModel> Packages);
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
| Task ID | State | Notes |
|
||||
| --- | --- | --- |
|
||||
| `SCANNER-CLI-0001` | DOING (2025-11-09) | Add Ruby-specific verbs/help, refresh docs & goldens per Sprint 138. |
|
||||
|
||||
| `SCANNER-CLI-0001` | DONE (2025-11-12) | Ruby verbs now consume the persisted `RubyPackageInventory`, warn when inventories are missing, and docs/tests were refreshed per Sprint 138. |
|
||||
|
||||
@@ -515,16 +515,18 @@ public sealed class CommandHandlersTests
|
||||
var originalExit = Environment.ExitCode;
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
RubyPackages = new[]
|
||||
{
|
||||
CreateRubyPackageArtifact("pkg-rack", "rack", "3.1.0", new[] { "default", "web" }, runtimeUsed: true),
|
||||
CreateRubyPackageArtifact("pkg-sidekiq", "sidekiq", "7.2.1", groups: null, runtimeUsed: false, metadataOverrides: new Dictionary<string, string?>
|
||||
RubyInventory = CreateRubyInventory(
|
||||
"scan-ruby",
|
||||
new[]
|
||||
{
|
||||
["groups"] = "jobs",
|
||||
["runtime.entrypoints"] = "config/jobs.rb",
|
||||
["runtime.files"] = "config/jobs.rb"
|
||||
CreateRubyPackageArtifact("pkg-rack", "rack", "3.1.0", new[] { "default", "web" }, runtimeUsed: true),
|
||||
CreateRubyPackageArtifact("pkg-sidekiq", "sidekiq", "7.2.1", groups: null, runtimeUsed: false, metadataOverrides: new Dictionary<string, string?>
|
||||
{
|
||||
["groups"] = "jobs",
|
||||
["runtime.entrypoints"] = "config/jobs.rb",
|
||||
["runtime.files"] = "config/jobs.rb"
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
var provider = BuildServiceProvider(backend);
|
||||
|
||||
@@ -557,15 +559,17 @@ public sealed class CommandHandlersTests
|
||||
public async Task HandleRubyResolveAsync_WritesJson()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
const string identifier = "ruby-scan-json";
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
|
||||
{
|
||||
RubyPackages = new[]
|
||||
{
|
||||
CreateRubyPackageArtifact("pkg-rack-json", "rack", "3.1.0", new[] { "default" }, runtimeUsed: true)
|
||||
}
|
||||
RubyInventory = CreateRubyInventory(
|
||||
identifier,
|
||||
new[]
|
||||
{
|
||||
CreateRubyPackageArtifact("pkg-rack-json", "rack", "3.1.0", new[] { "default" }, runtimeUsed: true)
|
||||
})
|
||||
};
|
||||
var provider = BuildServiceProvider(backend);
|
||||
const string identifier = "ruby-scan-json";
|
||||
|
||||
try
|
||||
{
|
||||
@@ -608,6 +612,35 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleRubyResolveAsync_NotifiesWhenInventoryMissing()
|
||||
{
|
||||
var originalExit = Environment.ExitCode;
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
|
||||
var provider = BuildServiceProvider(backend);
|
||||
|
||||
try
|
||||
{
|
||||
var output = await CaptureTestConsoleAsync(async _ =>
|
||||
{
|
||||
await CommandHandlers.HandleRubyResolveAsync(
|
||||
provider,
|
||||
imageReference: null,
|
||||
scanId: "scan-missing",
|
||||
format: "table",
|
||||
verbose: false,
|
||||
cancellationToken: CancellationToken.None);
|
||||
});
|
||||
|
||||
Assert.Equal(0, Environment.ExitCode);
|
||||
Assert.Contains("not available", output.Combined, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAdviseRunAsync_WritesOutputAndSetsExitCode()
|
||||
{
|
||||
@@ -3520,6 +3553,18 @@ spec:
|
||||
mergedMetadata);
|
||||
}
|
||||
|
||||
private static RubyPackageInventoryModel CreateRubyInventory(
|
||||
string scanId,
|
||||
IReadOnlyList<RubyPackageArtifactModel> packages,
|
||||
string? imageDigest = null)
|
||||
{
|
||||
return new RubyPackageInventoryModel(
|
||||
scanId,
|
||||
imageDigest ?? "sha256:inventory",
|
||||
DateTimeOffset.UtcNow,
|
||||
packages);
|
||||
}
|
||||
|
||||
|
||||
private static string ComputeSha256Base64(string path)
|
||||
{
|
||||
@@ -3601,8 +3646,8 @@ spec:
|
||||
public string? LastExcititorRoute { get; private set; }
|
||||
public HttpMethod? LastExcititorMethod { get; private set; }
|
||||
public object? LastExcititorPayload { get; private set; }
|
||||
public IReadOnlyList<RubyPackageArtifactModel> RubyPackages { get; set; } = Array.Empty<RubyPackageArtifactModel>();
|
||||
public Exception? RubyPackagesException { get; set; }
|
||||
public RubyPackageInventoryModel? RubyInventory { get; set; }
|
||||
public Exception? RubyInventoryException { get; set; }
|
||||
public string? LastRubyPackagesScanId { get; private set; }
|
||||
public List<(string ExportId, string DestinationPath, string? Algorithm, string? Digest)> ExportDownloads { get; } = new();
|
||||
public ExcititorOperationResult? ExcititorResult { get; set; } = new ExcititorOperationResult(true, "ok", null, null);
|
||||
@@ -3830,15 +3875,15 @@ spec:
|
||||
return Task.FromResult(EntryTraceResponse);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<RubyPackageArtifactModel>> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
|
||||
public Task<RubyPackageInventoryModel?> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRubyPackagesScanId = scanId;
|
||||
if (RubyPackagesException is not null)
|
||||
if (RubyInventoryException is not null)
|
||||
{
|
||||
throw RubyPackagesException;
|
||||
throw RubyInventoryException;
|
||||
}
|
||||
|
||||
return Task.FromResult(RubyPackages);
|
||||
return Task.FromResult(RubyInventory);
|
||||
}
|
||||
|
||||
public Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user