audit, advisories and doctors/setup work
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
10
src/Scanner/StellaOps.Scanner.Worker/TASKS.md
Normal file
10
src/Scanner/StellaOps.Scanner.Worker/TASKS.md
Normal 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. |
|
||||
Reference in New Issue
Block a user