audit, advisories and doctors/setup work

This commit is contained in:
master
2026-01-13 18:53:39 +02:00
parent 9ca7cb183e
commit d7be6ba34b
811 changed files with 54242 additions and 4056 deletions

View File

@@ -54,7 +54,7 @@ public sealed class PolicyFidelityCalculator
// Compare overall outcome
if (a.Passed != b.Passed)
differences.Add($"outcome:{a.Passed}{b.Passed}");
differences.Add($"outcome:{a.Passed}->{b.Passed}");
// Compare reason codes (order-independent)
var aReasons = a.ReasonCodes.OrderBy(r => r, StringComparer.Ordinal).ToList();
@@ -65,11 +65,11 @@ public sealed class PolicyFidelityCalculator
// Compare violation count
if (a.ViolationCount != b.ViolationCount)
differences.Add($"violations:{a.ViolationCount}{b.ViolationCount}");
differences.Add($"violations:{a.ViolationCount}->{b.ViolationCount}");
// Compare block level
if (!string.Equals(a.BlockLevel, b.BlockLevel, StringComparison.Ordinal))
differences.Add($"block_level:{a.BlockLevel}{b.BlockLevel}");
differences.Add($"block_level:{a.BlockLevel}->{b.BlockLevel}");
return (differences.Count == 0, differences);
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Security.Cryptography;
namespace StellaOps.Scanner.Worker.Determinism;
@@ -21,6 +22,13 @@ public sealed class DeterministicRandomProvider : IDeterministicRandomProvider
public Random Create()
{
return _seed.HasValue ? new Random(_seed.Value) : Random.Shared;
if (_seed.HasValue)
{
return new Random(_seed.Value);
}
Span<byte> seedBytes = stackalloc byte[4];
RandomNumberGenerator.Fill(seedBytes);
return new Random(BitConverter.ToInt32(seedBytes));
}
}

View File

@@ -127,7 +127,7 @@ public sealed partial class ScannerWorkerHostedService : BackgroundService
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
processingException = null;
await lease.AbandonAsync("host-stopping", CancellationToken.None).ConfigureAwait(false);
await lease.AbandonAsync("host-stopping", stoppingToken).ConfigureAwait(false);
JobAbandoned(_logger, lease.JobId, lease.ScanId);
}
catch (Exception ex)
@@ -140,13 +140,13 @@ public sealed partial class ScannerWorkerHostedService : BackgroundService
var maxAttempts = options.Queue.MaxAttempts;
if (lease.Attempt >= maxAttempts)
{
await lease.PoisonAsync(reason, CancellationToken.None).ConfigureAwait(false);
await lease.PoisonAsync(reason, stoppingToken).ConfigureAwait(false);
_metrics.IncrementJobFailed(context, reason);
JobPoisoned(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
}
else
{
await lease.AbandonAsync(reason, CancellationToken.None).ConfigureAwait(false);
await lease.AbandonAsync(reason, stoppingToken).ConfigureAwait(false);
JobAbandonedWithError(_logger, lease.JobId, lease.ScanId, lease.Attempt, maxAttempts, ex);
}
}

View File

@@ -37,7 +37,7 @@ internal sealed class ScannerStorageSurfaceSecretConfigurator : IConfigureOption
CasAccessSecret? secret = null;
try
{
using var handle = _secretProvider.GetAsync(request).AsTask().GetAwaiter().GetResult();
using var handle = _secretProvider.Get(request);
secret = SurfaceSecretParser.ParseCasAccessSecret(handle);
}
catch (SurfaceSecretNotFoundException)

View File

@@ -211,7 +211,7 @@ public class PoEOrchestrator
$"1. Build container image: {context.ImageDigest}",
$"2. Run scanner: stella scan --image {context.ImageDigest} --config {context.ConfigPath ?? "etc/scanner.yaml"}",
$"3. Extract reachability graph and resolve paths",
$"4. Resolve {subgraph.VulnId} {subgraph.ComponentRef} to vulnerable symbols",
$"4. Resolve {subgraph.VulnId} -> {subgraph.ComponentRef} to vulnerable symbols",
$"5. Compute paths from {subgraph.EntryRefs.Length} entry points to {subgraph.SinkRefs.Length} sinks"
};
}

View File

@@ -1,7 +1,7 @@
// -----------------------------------------------------------------------------
// BinaryFindingMapper.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-08 Create BinaryFindingMapper to convert matches to findings
// Task: SCANINT-08 - Create BinaryFindingMapper to convert matches to findings
// -----------------------------------------------------------------------------
using System.Collections.Immutable;

View File

@@ -1,7 +1,7 @@
// -----------------------------------------------------------------------------
// BinaryLookupStageExecutor.cs
// Sprint: SPRINT_20251226_014_BINIDX
// Task: SCANINT-02 Create IBinaryLookupStep in scan pipeline
// Task: SCANINT-02 - Create IBinaryLookupStep in scan pipeline
// -----------------------------------------------------------------------------
using System.Collections.Immutable;

View File

@@ -175,8 +175,9 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
cancellationToken)
.ConfigureAwait(false);
result = cacheEntry.Result;
if (cacheEntry.IsHit)
var (cachedResult, isHit) = cacheEntry;
result = cachedResult;
if (isHit)
{
_metrics.RecordOsCacheHit(context, analyzer.AnalyzerId);
}
@@ -292,8 +293,9 @@ internal sealed class CompositeScanAnalyzerDispatcher : IScanAnalyzerDispatcher
token => engine.AnalyzeAsync(analyzerContext, token),
cancellationToken)
.ConfigureAwait(false);
var result = cacheEntry.Result;
if (cacheEntry.IsHit)
var (cachedResult, isHit) = cacheEntry;
var result = cachedResult;
if (isHit)
{
_metrics.RecordLanguageCacheHit(context, analyzer.Id);
}

View File

@@ -9,6 +9,7 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Emit.Native;
using StellaOps.Scanner.Worker.Diagnostics;
@@ -25,6 +26,7 @@ public sealed class NativeAnalyzerExecutor
{
private readonly NativeBinaryDiscovery _discovery;
private readonly INativeComponentEmitter _emitter;
private readonly IElfSectionHashExtractor _sectionHashExtractor;
private readonly NativeAnalyzerOptions _options;
private readonly ILogger<NativeAnalyzerExecutor> _logger;
private readonly ScannerWorkerMetrics _metrics;
@@ -32,12 +34,14 @@ public sealed class NativeAnalyzerExecutor
public NativeAnalyzerExecutor(
NativeBinaryDiscovery discovery,
INativeComponentEmitter emitter,
IElfSectionHashExtractor sectionHashExtractor,
IOptions<NativeAnalyzerOptions> options,
ILogger<NativeAnalyzerExecutor> logger,
ScannerWorkerMetrics metrics)
{
_discovery = discovery ?? throw new ArgumentNullException(nameof(discovery));
_emitter = emitter ?? throw new ArgumentNullException(nameof(emitter));
_sectionHashExtractor = sectionHashExtractor ?? throw new ArgumentNullException(nameof(sectionHashExtractor));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
@@ -148,20 +152,26 @@ public sealed class NativeAnalyzerExecutor
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(_options.SingleBinaryTimeout);
return await Task.Run(() =>
{
// Read binary header to extract Build-ID and other metadata
var buildId = ExtractBuildId(binary);
var sectionHashes = binary.Format == BinaryFormat.Elf
? await _sectionHashExtractor.ExtractAsync(binary.AbsolutePath, cts.Token).ConfigureAwait(false)
: null;
return new NativeBinaryMetadata
{
Format = binary.Format.ToString().ToLowerInvariant(),
FilePath = binary.RelativePath,
BuildId = buildId,
Architecture = DetectArchitecture(binary),
Platform = DetectPlatform(binary)
};
}, cts.Token).ConfigureAwait(false);
cts.Token.ThrowIfCancellationRequested();
// Read binary header to extract Build-ID and other metadata
var buildId = ExtractBuildId(binary) ?? sectionHashes?.BuildId;
return new NativeBinaryMetadata
{
Format = binary.Format.ToString().ToLowerInvariant(),
FilePath = binary.RelativePath,
BuildId = buildId,
Architecture = DetectArchitecture(binary),
Platform = DetectPlatform(binary),
FileDigest = sectionHashes?.FileHash,
FileSize = binary.SizeBytes,
ElfSectionHashes = sectionHashes
};
}
catch (OperationCanceledException)
{

View File

@@ -38,7 +38,7 @@ public sealed class NativeBinaryDiscovery
/// <summary>
/// Discovers binaries in the specified root filesystem path.
/// </summary>
public async Task<IReadOnlyList<DiscoveredBinary>> DiscoverAsync(
public Task<IReadOnlyList<DiscoveredBinary>> DiscoverAsync(
string rootPath,
CancellationToken cancellationToken = default)
{
@@ -56,23 +56,20 @@ public sealed class NativeBinaryDiscovery
_options.BinaryExtensions.Select(e => e.StartsWith('.') ? e : "." + e),
StringComparer.OrdinalIgnoreCase);
await Task.Run(() =>
{
DiscoverRecursive(
rootPath,
rootPath,
discovered,
excludeSet,
extensionSet,
cancellationToken);
}, cancellationToken).ConfigureAwait(false);
DiscoverRecursive(
rootPath,
rootPath,
discovered,
excludeSet,
extensionSet,
cancellationToken);
_logger.LogInformation(
"Discovered {Count} native binaries in {RootPath}",
discovered.Count,
rootPath);
return discovered;
return Task.FromResult<IReadOnlyList<DiscoveredBinary>>(discovered);
}
private void DiscoverRecursive(

View File

@@ -150,18 +150,16 @@ public sealed class PoEGenerationStageExecutor : IScanStageExecutor
graphHash = casResult.GraphHash;
}
// Try to get build ID from surface manifest or other sources
string? buildId = null;
// TODO: Extract build ID from surface manifest or binary analysis
// Resolve build ID from metadata when available.
var metadata = context.Lease.Metadata;
var buildId = TryGetMetadataValue(metadata, "build.id", "buildId", "build-id", "scanner.build.id");
// Try to get image digest from scan job lease
string? imageDigest = null;
// TODO: Extract image digest from scan job
// Resolve image digest from scan job metadata.
var imageDigest = ResolveImageDigest(context);
// Try to get policy information
string? policyId = null;
string? policyDigest = null;
// TODO: Extract policy information from scan configuration
// Resolve policy identifiers from metadata.
var policyId = TryGetMetadataValue(metadata, "policy.id", "policyId", "scanner.policy.id");
var policyDigest = TryGetMetadataValue(metadata, "policy.digest", "policyDigest", "verdict.policy.digest");
// Get scanner version
var scannerVersion = typeof(PoEGenerationStageExecutor).Assembly.GetName().Version?.ToString() ?? "unknown";
@@ -180,6 +178,41 @@ public sealed class PoEGenerationStageExecutor : IScanStageExecutor
ConfigPath: configPath
);
}
private static string? ResolveImageDigest(ScanJobContext context)
{
var metadata = context.Lease.Metadata;
if (metadata.TryGetValue("image.digest", out var digest) && !string.IsNullOrWhiteSpace(digest))
{
return digest.Trim();
}
if (metadata.TryGetValue("imageDigest", out digest) && !string.IsNullOrWhiteSpace(digest))
{
return digest.Trim();
}
if (metadata.TryGetValue("scanner.image.digest", out digest) && !string.IsNullOrWhiteSpace(digest))
{
return digest.Trim();
}
return context.ImageDigest;
}
private static string? TryGetMetadataValue(IReadOnlyDictionary<string, string> metadata, params string[] keys)
{
foreach (var key in keys)
{
if (metadata.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
{
return value.Trim();
}
}
return null;
}
}
/// <summary>

View File

@@ -3,11 +3,11 @@ using System.Buffers.Text;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Attestation;
using StellaOps.Cryptography;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.Secrets;
@@ -71,12 +71,14 @@ internal sealed class HmacDsseEnvelopeSigner : IDsseEnvelopeSigner
public Task<DsseEnvelope> SignAsync(string payloadType, ReadOnlyMemory<byte> content, string suggestedKind, string merkleRoot, string? view, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (_secretBytes is null)
{
return _deterministic.SignAsync(payloadType, content, suggestedKind, merkleRoot, view, cancellationToken);
}
var pae = BuildPae(payloadType, content.Span);
var pae = DsseHelper.PreAuthenticationEncoding(payloadType, content.Span);
var signatureBytes = _cryptoHmac.ComputeHmacForPurpose(_secretBytes, pae, HmacPurpose.Signing);
var envelope = new
{
@@ -88,12 +90,7 @@ internal sealed class HmacDsseEnvelopeSigner : IDsseEnvelopeSigner
}
};
var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false
});
var bytes = Encoding.UTF8.GetBytes(json);
var bytes = DsseEnvelopeSerializer.Serialize(envelope);
var digest = $"sha256:{ComputeSha256Hex(content.Span)}";
var uri = $"cas://attestations/{suggestedKind}/{digest}.json";
@@ -134,7 +131,7 @@ internal sealed class HmacDsseEnvelopeSigner : IDsseEnvelopeSigner
Component: "scanner-worker",
SecretType: "attestation",
Name: "dsse-signing");
using var handle = provider.GetAsync(request, CancellationToken.None).GetAwaiter().GetResult();
using var handle = provider.Get(request);
var bytes = handle.AsBytes();
return bytes.IsEmpty ? null : bytes.Span.ToArray();
}
@@ -187,37 +184,6 @@ internal sealed class HmacDsseEnvelopeSigner : IDsseEnvelopeSigner
return null;
}
private static byte[] BuildPae(string payloadType, ReadOnlySpan<byte> payload)
{
const string prefix = "DSSEv1";
var typeBytes = Encoding.UTF8.GetBytes(payloadType);
var typeLen = Encoding.UTF8.GetBytes(typeBytes.Length.ToString());
var payloadLen = Encoding.UTF8.GetBytes(payload.Length.ToString());
var total = prefix.Length + 1 + typeLen.Length + 1 + typeBytes.Length + 1 + payloadLen.Length + 1 + payload.Length;
var buffer = new byte[total];
var offset = 0;
Encoding.UTF8.GetBytes(prefix, buffer.AsSpan(offset));
offset += prefix.Length;
buffer[offset++] = 0x20;
typeLen.CopyTo(buffer.AsSpan(offset));
offset += typeLen.Length;
buffer[offset++] = 0x20;
typeBytes.CopyTo(buffer.AsSpan(offset));
offset += typeBytes.Length;
buffer[offset++] = 0x20;
payloadLen.CopyTo(buffer.AsSpan(offset));
offset += payloadLen.Length;
buffer[offset++] = 0x20;
payload.CopyTo(buffer.AsSpan(offset));
return buffer;
}
private static string ComputeSha256Hex(ReadOnlySpan<byte> data)
{
Span<byte> hash = stackalloc byte[32];

View File

@@ -1,7 +1,10 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Canonical.Json;
namespace StellaOps.Scanner.Worker.Processing.Surface;
@@ -12,6 +15,19 @@ internal interface IDsseEnvelopeSigner
Task<DsseEnvelope> SignAsync(string payloadType, ReadOnlyMemory<byte> content, string suggestedKind, string merkleRoot, string? view, CancellationToken cancellationToken);
}
internal static class DsseEnvelopeSerializer
{
private static readonly JsonSerializerOptions EnvelopeOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = null,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
public static byte[] Serialize<T>(T value) => CanonJson.Canonicalize(value, EnvelopeOptions);
}
/// <summary>
/// Deterministic fallback signer that encodes sha256 hash as the signature. Replace with real Attestor/Signer when available.
/// </summary>
@@ -19,6 +35,8 @@ internal sealed class DeterministicDsseEnvelopeSigner : IDsseEnvelopeSigner
{
public Task<DsseEnvelope> SignAsync(string payloadType, ReadOnlyMemory<byte> content, string suggestedKind, string merkleRoot, string? view, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var signature = ComputeSha256Hex(content.Span);
var envelope = new
{
@@ -30,12 +48,7 @@ internal sealed class DeterministicDsseEnvelopeSigner : IDsseEnvelopeSigner
}
};
var json = JsonSerializer.Serialize(envelope, new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false
});
var bytes = Encoding.UTF8.GetBytes(json);
var bytes = DsseEnvelopeSerializer.Serialize(envelope);
var digest = $"sha256:{signature}";
var uri = $"cas://attestations/{suggestedKind}/{signature}.json";

View File

@@ -5,9 +5,11 @@ using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Canonical.Json;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Entropy;
@@ -23,10 +25,12 @@ namespace StellaOps.Scanner.Worker.Processing.Surface;
internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
PropertyNamingPolicy = null,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web)
@@ -92,8 +96,9 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return;
}
var determinismPayloads = BuildDeterminismPayloads(context, payloads, out var merkleRoot);
if (determinismPayloads is not null && determinismPayloads.Count > 0)
var (determinismPayloads, merkleRoot) = await BuildDeterminismPayloadsAsync(context, payloads, cancellationToken)
.ConfigureAwait(false);
if (determinismPayloads.Count > 0)
{
payloads.AddRange(determinismPayloads);
}
@@ -191,13 +196,13 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
var fragments = context.Analysis.GetLayerFragments();
if (!fragments.IsDefaultOrEmpty && fragments.Length > 0)
{
var fragmentsJson = JsonSerializer.Serialize(fragments, JsonOptions);
var fragmentsBytes = SerializeCanonical(fragments);
payloads.Add(new SurfaceManifestPayload(
ArtifactDocumentType.SurfaceLayerFragment,
ArtifactDocumentFormat.ComponentFragmentJson,
Kind: "layer.fragments",
MediaType: "application/json",
Content: Encoding.UTF8.GetBytes(fragmentsJson),
Content: fragmentsBytes,
View: "inventory"));
}
@@ -217,13 +222,13 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
if (context.Analysis.TryGet<EntropyReport>(ScanAnalysisKeys.EntropyReport, out var entropyReport) && entropyReport is not null)
{
var json = JsonSerializer.Serialize(entropyReport, JsonOptions);
var entropyBytes = SerializeCanonical(entropyReport);
payloads.Add(new SurfaceManifestPayload(
ArtifactDocumentType.SurfaceObservation,
ArtifactDocumentFormat.ObservationJson,
Kind: "entropy.report",
MediaType: "application/json",
Content: Encoding.UTF8.GetBytes(json),
Content: entropyBytes,
View: "entropy",
Metadata: new Dictionary<string, string>
{
@@ -235,13 +240,13 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
if (context.Analysis.TryGet<EntropyLayerSummary>(ScanAnalysisKeys.EntropyLayerSummary, out var entropySummary) && entropySummary is not null)
{
var json = JsonSerializer.Serialize(entropySummary, JsonOptions);
var summaryBytes = SerializeCanonical(entropySummary);
payloads.Add(new SurfaceManifestPayload(
ArtifactDocumentType.SurfaceObservation,
ArtifactDocumentFormat.ObservationJson,
Kind: "entropy.layer-summary",
MediaType: "application/json",
Content: Encoding.UTF8.GetBytes(json),
Content: summaryBytes,
View: "entropy",
Metadata: new Dictionary<string, string>
{
@@ -253,9 +258,11 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return payloads;
}
private IReadOnlyList<SurfaceManifestPayload> BuildDeterminismPayloads(ScanJobContext context, IEnumerable<SurfaceManifestPayload> payloads, out string? merkleRoot)
private async Task<(IReadOnlyList<SurfaceManifestPayload> Payloads, string? MerkleRoot)> BuildDeterminismPayloadsAsync(
ScanJobContext context,
IEnumerable<SurfaceManifestPayload> payloads,
CancellationToken cancellationToken)
{
merkleRoot = null;
var pins = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (context.Lease.Metadata.TryGetValue("determinism.feed", out var feed) && !string.IsNullOrWhiteSpace(feed))
{
@@ -268,7 +275,6 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
}
var (artifactHashes, recipeBytes, recipeSha256) = BuildCompositionRecipe(payloads);
merkleRoot = recipeSha256;
var report = new
{
@@ -285,10 +291,10 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
var evidence = new Determinism.DeterminismEvidence(artifactHashes, recipeSha256);
context.Analysis.Set(ScanAnalysisKeys.DeterminismEvidence, evidence);
var payloadList = payloads.ToList();
var additions = new List<SurfaceManifestPayload>();
// Publish composition recipe as a manifest artifact for offline replay.
payloadList.Add(new SurfaceManifestPayload(
additions.Add(new SurfaceManifestPayload(
ArtifactDocumentType.CompositionRecipe,
ArtifactDocumentFormat.CompositionRecipeJson,
Kind: "composition.recipe",
@@ -301,14 +307,14 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
}));
// Attach DSSE envelope for the recipe (deterministic local signature = sha256 hash bytes).
var recipeDsse = _dsseSigner.SignAsync(
var recipeDsse = await _dsseSigner.SignAsync(
payloadType: "application/vnd.stellaops.composition.recipe+json",
content: recipeBytes,
suggestedKind: "composition.recipe.dsse",
merkleRoot: recipeSha256,
view: null,
cancellationToken: CancellationToken.None).Result;
payloadList.Add(new SurfaceManifestPayload(
cancellationToken: cancellationToken).ConfigureAwait(false);
additions.Add(new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: "composition.recipe.dsse",
@@ -321,17 +327,17 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
}));
// Attach DSSE envelope for layer fragments when present.
foreach (var fragmentPayload in payloadList.Where(p => p.Kind == "layer.fragments").ToArray())
foreach (var fragmentPayload in payloads.Where(p => p.Kind == "layer.fragments").ToArray())
{
var dsse = _dsseSigner.SignAsync(
var dsse = await _dsseSigner.SignAsync(
payloadType: fragmentPayload.MediaType,
content: fragmentPayload.Content,
suggestedKind: "layer.fragments.dsse",
merkleRoot: recipeSha256,
view: fragmentPayload.View,
cancellationToken: CancellationToken.None).Result;
cancellationToken: cancellationToken).ConfigureAwait(false);
payloadList.Add(new SurfaceManifestPayload(
additions.Add(new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: "layer.fragments.dsse",
@@ -345,16 +351,16 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
}));
}
var json = JsonSerializer.Serialize(report, JsonOptions);
payloadList.Add(new SurfaceManifestPayload(
var reportBytes = SerializeCanonical(report);
additions.Add(new SurfaceManifestPayload(
ArtifactDocumentType.SurfaceObservation,
ArtifactDocumentFormat.ObservationJson,
Kind: "determinism.json",
MediaType: "application/json",
Content: Encoding.UTF8.GetBytes(json),
Content: reportBytes,
View: "replay"));
return payloadList.Skip(payloads.Count()).ToList();
return (additions, recipeSha256);
}
private (Dictionary<string, string> Hashes, byte[] RecipeBytes, string RecipeSha256) BuildCompositionRecipe(IEnumerable<SurfaceManifestPayload> payloads)
@@ -373,8 +379,7 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
artifacts = map, // already sorted
};
var recipeJson = JsonSerializer.Serialize(recipe, JsonOptions);
var recipeBytes = Encoding.UTF8.GetBytes(recipeJson);
var recipeBytes = SerializeCanonical(recipe);
var merkleRoot = _hash.ComputeHashHex(recipeBytes, HashAlgorithms.Sha256);
return (new Dictionary<string, string>(map, StringComparer.OrdinalIgnoreCase), recipeBytes, merkleRoot);
@@ -402,13 +407,12 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
}
};
var json = JsonSerializer.Serialize(envelope, JsonOptions);
return new SurfaceManifestPayload(
ArtifactDocumentType.Attestation,
ArtifactDocumentFormat.DsseJson,
Kind: kind,
MediaType: mediaType,
Content: Encoding.UTF8.GetBytes(json),
Content: SerializeCanonical(envelope),
Metadata: new Dictionary<string, string>
{
["merkleRoot"] = merkleRoot,
@@ -670,6 +674,11 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return normalized.Count == 0 ? null : normalized;
}
private static byte[] SerializeCanonical<T>(T value)
{
return CanonJson.Canonicalize(value, CanonicalJsonOptions);
}
private string ComputeDigest(ReadOnlySpan<byte> content)
{
var hex = _hash.ComputeHashHex(content, HashAlgorithms.Sha256);

View File

@@ -12,6 +12,7 @@ using StellaOps.Scanner.Reachability;
using StellaOps.Scanner.Reachability.Gates;
using StellaOps.Scanner.Analyzers.OS.Plugin;
using StellaOps.Scanner.Analyzers.Lang.Plugin;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Index;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.Core.Contracts;
@@ -55,6 +56,8 @@ builder.Services.AddOptions<NativeAnalyzerOptions>()
.BindConfiguration(NativeAnalyzerOptions.SectionName)
.ValidateOnStart();
builder.Services.AddNativeAnalyzer(builder.Configuration);
builder.Services.AddSingleton<IValidateOptions<ScannerWorkerOptions>, ScannerWorkerOptionsValidator>();
var workerOptions = builder.Configuration.GetSection(ScannerWorkerOptions.SectionName).Get<ScannerWorkerOptions>() ?? new ScannerWorkerOptions();

View File

@@ -15,10 +15,12 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Process" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../__Libraries/StellaOps.Canonical.Json/StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Determinism.Abstractions/StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Plugin/StellaOps.Plugin.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<ProjectReference Include="../../Attestor/StellaOps.Attestation/StellaOps.Attestation.csproj" />
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Auth.Client/StellaOps.Auth.Client.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Analyzers.OS/StellaOps.Scanner.Analyzers.OS.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />

View File

@@ -0,0 +1,10 @@
# Scanner Worker Task Board
This board mirrors active sprint tasks for this module.
Source of truth: `docs/implplan/SPRINT_20260113_001_001_SCANNER_elf_section_hashes.md`.
| Task ID | Status | Notes |
| --- | --- | --- |
| ELF-SECTION-EVIDENCE-0001 | DONE | Populate section hashes into native metadata for SBOM emission. |
| ELF-SECTION-DI-0001 | DONE | Register section hash extractor options and services. |
| AUDIT-HOTLIST-SCANNER-WORKER-0001 | DONE | Apply audit hotlist findings for Scanner.Worker. |