feat: add security sink detection patterns for JavaScript/TypeScript

- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations).
- Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns.
- Added `package-lock.json` for dependency management.
This commit is contained in:
StellaOps Bot
2025-12-22 23:21:21 +02:00
parent 3ba7157b00
commit 5146204f1b
529 changed files with 73579 additions and 5985 deletions

View File

@@ -0,0 +1,202 @@
// -----------------------------------------------------------------------------
// VerdictPushDiagnostics.cs
// Sprint: SPRINT_4300_0001_0001_oci_verdict_attestation_push
// Task: VERDICT-009
// Description: OpenTelemetry instrumentation for verdict push operations.
// -----------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace StellaOps.Scanner.Storage.Oci.Diagnostics;
/// <summary>
/// OpenTelemetry instrumentation for verdict push operations.
/// Provides activity tracing and metrics for observability.
/// </summary>
public static class VerdictPushDiagnostics
{
/// <summary>
/// Activity source name for verdict push operations.
/// </summary>
public const string ActivitySourceName = "StellaOps.Scanner.VerdictPush";
/// <summary>
/// Activity source version.
/// </summary>
public const string ActivityVersion = "1.0.0";
/// <summary>
/// Meter name for verdict push metrics.
/// </summary>
public const string MeterName = "stellaops.scanner.verdict_push";
/// <summary>
/// Meter version.
/// </summary>
public const string MeterVersion = "1.0.0";
/// <summary>
/// Activity source for verdict push tracing.
/// </summary>
public static ActivitySource ActivitySource { get; } = new(ActivitySourceName, ActivityVersion);
/// <summary>
/// Meter for verdict push metrics.
/// </summary>
public static Meter Meter { get; } = new(MeterName, MeterVersion);
// Counters
private static readonly Counter<long> _pushAttempts = Meter.CreateCounter<long>(
"stellaops.verdict.push.attempts",
unit: "{attempts}",
description: "Total number of verdict push attempts");
private static readonly Counter<long> _pushSuccesses = Meter.CreateCounter<long>(
"stellaops.verdict.push.successes",
unit: "{successes}",
description: "Total number of successful verdict pushes");
private static readonly Counter<long> _pushFailures = Meter.CreateCounter<long>(
"stellaops.verdict.push.failures",
unit: "{failures}",
description: "Total number of failed verdict pushes");
private static readonly Counter<long> _pushRetries = Meter.CreateCounter<long>(
"stellaops.verdict.push.retries",
unit: "{retries}",
description: "Total number of verdict push retries");
// Histograms
private static readonly Histogram<double> _pushDuration = Meter.CreateHistogram<double>(
"stellaops.verdict.push.duration",
unit: "ms",
description: "Duration of verdict push operations in milliseconds");
private static readonly Histogram<long> _payloadSize = Meter.CreateHistogram<long>(
"stellaops.verdict.push.payload_size",
unit: "By",
description: "Size of verdict payload in bytes");
/// <summary>
/// Start an activity for a verdict push operation.
/// </summary>
public static Activity? StartPushActivity(
string imageReference,
string? imageDigest = null,
string? registry = null)
{
var activity = ActivitySource.StartActivity("verdict.push", ActivityKind.Client);
if (activity is null)
{
return null;
}
activity.SetTag("stellaops.verdict.image_reference", imageReference);
if (!string.IsNullOrWhiteSpace(imageDigest))
{
activity.SetTag("stellaops.verdict.image_digest", imageDigest);
}
if (!string.IsNullOrWhiteSpace(registry))
{
activity.SetTag("stellaops.verdict.registry", registry);
}
return activity;
}
/// <summary>
/// Record a push attempt.
/// </summary>
public static void RecordPushAttempt(string registry, string decision)
{
_pushAttempts.Add(1,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision));
}
/// <summary>
/// Record a successful push.
/// </summary>
public static void RecordPushSuccess(string registry, string decision, double durationMs, long payloadBytes)
{
_pushSuccesses.Add(1,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision));
_pushDuration.Record(durationMs,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision),
new KeyValuePair<string, object?>("status", "success"));
_payloadSize.Record(payloadBytes,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision));
}
/// <summary>
/// Record a failed push.
/// </summary>
public static void RecordPushFailure(string registry, string decision, string errorType, double durationMs)
{
_pushFailures.Add(1,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision),
new KeyValuePair<string, object?>("error_type", errorType));
_pushDuration.Record(durationMs,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("decision", decision),
new KeyValuePair<string, object?>("status", "failure"));
}
/// <summary>
/// Record a push retry.
/// </summary>
public static void RecordPushRetry(string registry, int attemptNumber, string reason)
{
_pushRetries.Add(1,
new KeyValuePair<string, object?>("registry", registry),
new KeyValuePair<string, object?>("attempt", attemptNumber),
new KeyValuePair<string, object?>("reason", reason));
}
/// <summary>
/// Set activity status to error.
/// </summary>
public static void SetActivityError(Activity? activity, Exception exception)
{
if (activity is null)
{
return;
}
activity.SetStatus(ActivityStatusCode.Error, exception.Message);
activity.SetTag("otel.status_code", "ERROR");
activity.SetTag("otel.status_description", exception.Message);
activity.SetTag("exception.type", exception.GetType().FullName);
activity.SetTag("exception.message", exception.Message);
}
/// <summary>
/// Set activity status to success.
/// </summary>
public static void SetActivitySuccess(Activity? activity, string? manifestDigest = null)
{
if (activity is null)
{
return;
}
activity.SetStatus(ActivityStatusCode.Ok);
activity.SetTag("otel.status_code", "OK");
if (!string.IsNullOrWhiteSpace(manifestDigest))
{
activity.SetTag("stellaops.verdict.manifest_digest", manifestDigest);
}
}
}

View File

@@ -14,4 +14,46 @@ public static class OciAnnotations
public const string StellaAfterDigest = "org.stellaops.delta.after.digest";
public const string StellaSbomDigest = "org.stellaops.sbom.digest";
public const string StellaVerdictDigest = "org.stellaops.verdict.digest";
// Sprint: SPRINT_4300_0001_0001 - OCI Verdict Attestation Push
/// <summary>
/// The final decision (pass, warn, block) for the verdict.
/// </summary>
public const string StellaVerdictDecision = "org.stellaops.verdict.decision";
/// <summary>
/// Digest of the feeds snapshot used for vulnerability matching.
/// </summary>
public const string StellaFeedsDigest = "org.stellaops.feeds.digest";
/// <summary>
/// Digest of the policy bundle used for evaluation.
/// </summary>
public const string StellaPolicyDigest = "org.stellaops.policy.digest";
/// <summary>
/// Graph revision identifier for the scan.
/// </summary>
public const string StellaGraphRevisionId = "org.stellaops.graph.revision.id";
/// <summary>
/// Digest of the proof bundle containing the evidence chain.
/// </summary>
public const string StellaProofBundleDigest = "org.stellaops.proof.bundle.digest";
/// <summary>
/// Timestamp when the verdict was computed.
/// </summary>
public const string StellaVerdictTimestamp = "org.stellaops.verdict.timestamp";
// Sprint: SPRINT_4300_0002_0002 - Unknowns Attestation Predicates
/// <summary>
/// Digest of the uncertainty state attestation.
/// </summary>
public const string StellaUncertaintyDigest = "org.stellaops.uncertainty.digest";
/// <summary>
/// Digest of the uncertainty budget attestation.
/// </summary>
public const string StellaUncertaintyBudgetDigest = "org.stellaops.uncertainty.budget.digest";
}

View File

@@ -14,4 +14,16 @@ public static class OciMediaTypes
public const string ReachabilitySlice = "application/vnd.stellaops.slice.v1+json";
public const string SliceConfig = "application/vnd.stellaops.slice.config.v1+json";
public const string SliceArtifact = "application/vnd.stellaops.slice.v1+json";
// Sprint: SPRINT_4300_0001_0001 - OCI Verdict Attestation Push
/// <summary>
/// Media type for risk verdict attestation artifacts.
/// These are pushed as OCI referrers for container images.
/// </summary>
public const string VerdictAttestation = "application/vnd.stellaops.verdict.v1+json";
/// <summary>
/// Config media type for verdict attestation artifacts.
/// </summary>
public const string VerdictConfig = "application/vnd.stellaops.verdict.config.v1+json";
}

View File

@@ -73,4 +73,17 @@ public sealed record OciRegistryAuthorization
break;
}
}
/// <summary>
/// Asynchronously authorizes a request. This is a convenience method that wraps ApplyTo.
/// The OciImageReference parameter is for future token refresh support.
/// </summary>
public Task AuthorizeRequestAsync(
HttpRequestMessage request,
OciImageReference reference,
CancellationToken cancellationToken = default)
{
ApplyTo(request);
return Task.CompletedTask;
}
}

View File

@@ -4,7 +4,6 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Reachability.Slices;
namespace StellaOps.Scanner.Storage.Oci.Offline;
@@ -95,15 +94,48 @@ public sealed record BundleImportResult
public string? Error { get; init; }
}
/// <summary>
/// Data transfer object for slice data in offline bundles.
/// Decoupled from ReachabilitySlice to avoid circular dependencies.
/// </summary>
public sealed record SliceDataDto
{
/// <summary>
/// Raw JSON bytes of the slice.
/// </summary>
public required byte[] JsonBytes { get; init; }
/// <summary>
/// CVE ID extracted from slice query (for annotations).
/// </summary>
public string? CveId { get; init; }
/// <summary>
/// Verdict status (for annotations).
/// </summary>
public string? VerdictStatus { get; init; }
/// <summary>
/// Referenced call graph digest.
/// </summary>
public string? GraphDigest { get; init; }
/// <summary>
/// Referenced SBOM digest.
/// </summary>
public string? SbomDigest { get; init; }
}
/// <summary>
/// Provider interface for slice storage operations.
/// Uses SliceDataDto to avoid circular dependencies with Reachability project.
/// </summary>
public interface ISliceStorageProvider
{
Task<IReadOnlyList<ReachabilitySlice>> GetSlicesForScanAsync(string scanId, CancellationToken cancellationToken = default);
Task<IReadOnlyList<SliceDataDto>> GetSlicesForScanAsync(string scanId, CancellationToken cancellationToken = default);
Task<byte[]?> GetGraphAsync(string digest, CancellationToken cancellationToken = default);
Task<byte[]?> GetSbomAsync(string digest, CancellationToken cancellationToken = default);
Task StoreSliceAsync(ReachabilitySlice slice, CancellationToken cancellationToken = default);
Task StoreSliceAsync(byte[] sliceJsonBytes, CancellationToken cancellationToken = default);
Task StoreGraphAsync(string digest, byte[] data, CancellationToken cancellationToken = default);
Task StoreSbomAsync(string digest, byte[] data, CancellationToken cancellationToken = default);
}
@@ -183,8 +215,7 @@ public sealed class OfflineBundleService
// Export slices
foreach (var slice in slices)
{
var sliceJson = JsonSerializer.Serialize(slice, JsonOptions);
var sliceBytes = Encoding.UTF8.GetBytes(sliceJson);
var sliceBytes = slice.JsonBytes;
var sliceDigest = ComputeDigest(sliceBytes);
var slicePath = Path.Combine(blobsDir, sliceDigest);
@@ -197,8 +228,8 @@ public sealed class OfflineBundleService
Size = sliceBytes.Length,
Path = $"{BlobsDirectory}/{sliceDigest}",
Annotations = ImmutableDictionary<string, string>.Empty
.Add("stellaops.slice.cveId", slice.Query?.CveId ?? "unknown")
.Add("stellaops.slice.verdict", slice.Verdict?.Status.ToString() ?? "unknown")
.Add("stellaops.slice.cveId", slice.CveId ?? "unknown")
.Add("stellaops.slice.verdict", slice.VerdictStatus ?? "unknown")
});
// Collect referenced graphs and SBOMs
@@ -435,12 +466,9 @@ public sealed class OfflineBundleService
if (artifact.MediaType == OciMediaTypes.ReachabilitySlice)
{
var slice = JsonSerializer.Deserialize<ReachabilitySlice>(data, JsonOptions);
if (slice != null)
{
await _storage.StoreSliceAsync(slice, cancellationToken).ConfigureAwait(false);
slicesImported++;
}
// Store raw JSON bytes - consumer deserializes to specific type
await _storage.StoreSliceAsync(data, cancellationToken).ConfigureAwait(false);
slicesImported++;
}
else if (artifact.MediaType == OciMediaTypes.ReachabilitySubgraph)
{

View File

@@ -3,7 +3,6 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Reachability.Slices;
namespace StellaOps.Scanner.Storage.Oci;
@@ -39,7 +38,11 @@ public sealed record SlicePullOptions
public sealed record SlicePullResult
{
public required bool Success { get; init; }
public ReachabilitySlice? Slice { get; init; }
/// <summary>
/// Raw slice data as JSON element (decoupled from ReachabilitySlice type).
/// Consumer should deserialize to appropriate type.
/// </summary>
public JsonElement? SliceData { get; init; }
public string? SliceDigest { get; init; }
public byte[]? DsseEnvelope { get; init; }
public string? Error { get; init; }
@@ -96,7 +99,7 @@ public sealed class SlicePullService : IDisposable
return new SlicePullResult
{
Success = true,
Slice = cached!.Slice,
SliceData = cached!.SliceData,
SliceDigest = digest,
DsseEnvelope = cached.DsseEnvelope,
FromCache = true,
@@ -185,9 +188,14 @@ public sealed class SlicePullService : IDisposable
};
}
// Parse slice
var slice = JsonSerializer.Deserialize<ReachabilitySlice>(sliceBytes, JsonOptions);
if (slice == null)
// Parse slice as raw JSON element (decoupled from ReachabilitySlice type)
JsonElement sliceData;
try
{
using var doc = JsonDocument.Parse(sliceBytes);
sliceData = doc.RootElement.Clone();
}
catch (JsonException)
{
return new SlicePullResult
{
@@ -216,7 +224,7 @@ public sealed class SlicePullService : IDisposable
{
AddToCache(cacheKey, new CachedSlice
{
Slice = slice,
SliceData = sliceData,
DsseEnvelope = dsseEnvelope,
SignatureVerified = signatureVerified,
ExpiresAt = DateTimeOffset.UtcNow.Add(_options.CacheTtl)
@@ -230,7 +238,7 @@ public sealed class SlicePullService : IDisposable
return new SlicePullResult
{
Success = true,
Slice = slice,
SliceData = sliceData,
SliceDigest = digest,
DsseEnvelope = dsseEnvelope,
FromCache = false,
@@ -346,7 +354,7 @@ public sealed class SlicePullService : IDisposable
var index = await response.Content.ReadFromJsonAsync<OciReferrersIndex>(JsonOptions, cancellationToken)
.ConfigureAwait(false);
return index?.Manifests ?? Array.Empty<OciReferrer>();
return (IReadOnlyList<OciReferrer>?)index?.Manifests ?? Array.Empty<OciReferrer>();
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
@@ -430,7 +438,7 @@ public sealed class SlicePullService : IDisposable
private sealed record CachedSlice
{
public required ReachabilitySlice Slice { get; init; }
public required JsonElement SliceData { get; init; }
public byte[]? DsseEnvelope { get; init; }
public bool SignatureVerified { get; init; }
public required DateTimeOffset ExpiresAt { get; init; }

View File

@@ -8,6 +8,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup>
<!-- NOTE: Reachability reference intentionally removed to break circular dependency:
Reachability -> SmartDiff -> Storage.Oci -> Reachability
Use SliceDataDto and JsonElement instead of ReachabilitySlice type. -->
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,287 @@
// -----------------------------------------------------------------------------
// VerdictOciPublisher.cs
// Sprint: SPRINT_4300_0001_0001_oci_verdict_attestation_push
// Task: VERDICT-009 - OpenTelemetry instrumentation integrated.
// Description: Pushes risk verdict attestations as OCI referrer artifacts.
// -----------------------------------------------------------------------------
using System.Diagnostics;
using StellaOps.Scanner.Storage.Oci.Diagnostics;
namespace StellaOps.Scanner.Storage.Oci;
/// <summary>
/// Request to push a verdict attestation to an OCI registry.
/// </summary>
public sealed record VerdictOciPublishRequest
{
/// <summary>
/// OCI image reference to attach the verdict to.
/// Format: registry/repository@sha256:digest
/// </summary>
public required string Reference { get; init; }
/// <summary>
/// Digest of the container image this verdict applies to.
/// </summary>
public required string ImageDigest { get; init; }
/// <summary>
/// The DSSE envelope bytes containing the signed verdict statement.
/// </summary>
public required byte[] DsseEnvelopeBytes { get; init; }
/// <summary>
/// Digest of the SBOM used for vulnerability matching.
/// </summary>
public required string SbomDigest { get; init; }
/// <summary>
/// Digest of the advisory feeds snapshot used.
/// </summary>
public required string FeedsDigest { get; init; }
/// <summary>
/// Digest of the policy bundle used for evaluation.
/// </summary>
public required string PolicyDigest { get; init; }
/// <summary>
/// The final verdict decision: pass, warn, or block.
/// </summary>
public required string Decision { get; init; }
/// <summary>
/// Graph revision ID for the scan.
/// </summary>
public string? GraphRevisionId { get; init; }
/// <summary>
/// Digest of the proof bundle containing the evidence chain.
/// </summary>
public string? ProofBundleDigest { get; init; }
/// <summary>
/// Digest of the attestation itself (for cross-referencing).
/// </summary>
public string? AttestationDigest { get; init; }
/// <summary>
/// When the verdict was computed.
/// </summary>
public DateTimeOffset? VerdictTimestamp { get; init; }
/// <summary>
/// Optional: Digest of the uncertainty state attestation.
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
/// </summary>
public string? UncertaintyStatementDigest { get; init; }
/// <summary>
/// Optional: Digest of the uncertainty budget attestation.
/// Sprint: SPRINT_4300_0002_0002_unknowns_attestation_predicates
/// </summary>
public string? UncertaintyBudgetDigest { get; init; }
}
/// <summary>
/// Service for pushing risk verdict attestations as OCI referrer artifacts.
/// This enables verdicts to be portable "ship tokens" attached to container images.
/// </summary>
public sealed class VerdictOciPublisher
{
private readonly OciArtifactPusher _pusher;
public VerdictOciPublisher(OciArtifactPusher pusher)
{
_pusher = pusher ?? throw new ArgumentNullException(nameof(pusher));
}
/// <summary>
/// Push a verdict attestation as an OCI referrer artifact.
/// </summary>
/// <param name="request">The verdict push request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The result of the push operation.</returns>
public async Task<OciArtifactPushResult> PushAsync(
VerdictOciPublishRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
// Extract registry from reference for telemetry
var registry = ExtractRegistry(request.Reference);
var payloadSize = request.DsseEnvelopeBytes.Length;
// Start activity for distributed tracing
using var activity = VerdictPushDiagnostics.StartPushActivity(
request.Reference,
request.ImageDigest,
registry);
// Record push attempt
VerdictPushDiagnostics.RecordPushAttempt(registry, request.Decision);
var stopwatch = Stopwatch.StartNew();
try
{
var annotations = new Dictionary<string, string>(StringComparer.Ordinal)
{
[OciAnnotations.StellaPredicateType] = VerdictPredicateTypes.Verdict,
[OciAnnotations.StellaSbomDigest] = request.SbomDigest,
[OciAnnotations.StellaFeedsDigest] = request.FeedsDigest,
[OciAnnotations.StellaPolicyDigest] = request.PolicyDigest,
[OciAnnotations.StellaVerdictDecision] = request.Decision
};
if (!string.IsNullOrWhiteSpace(request.GraphRevisionId))
{
annotations[OciAnnotations.StellaGraphRevisionId] = request.GraphRevisionId!;
}
if (!string.IsNullOrWhiteSpace(request.ProofBundleDigest))
{
annotations[OciAnnotations.StellaProofBundleDigest] = request.ProofBundleDigest!;
}
if (!string.IsNullOrWhiteSpace(request.AttestationDigest))
{
annotations[OciAnnotations.StellaAttestationDigest] = request.AttestationDigest!;
}
if (request.VerdictTimestamp.HasValue)
{
annotations[OciAnnotations.StellaVerdictTimestamp] = request.VerdictTimestamp.Value.ToString("O");
}
// Sprint: SPRINT_4300_0002_0002 - Unknowns Attestation Predicates
if (!string.IsNullOrWhiteSpace(request.UncertaintyStatementDigest))
{
annotations[OciAnnotations.StellaUncertaintyDigest] = request.UncertaintyStatementDigest!;
}
if (!string.IsNullOrWhiteSpace(request.UncertaintyBudgetDigest))
{
annotations[OciAnnotations.StellaUncertaintyBudgetDigest] = request.UncertaintyBudgetDigest!;
}
var pushRequest = new OciArtifactPushRequest
{
Reference = request.Reference,
ArtifactType = OciMediaTypes.VerdictAttestation,
SubjectDigest = request.ImageDigest,
Layers =
[
new OciLayerContent
{
Content = request.DsseEnvelopeBytes,
MediaType = OciMediaTypes.DsseEnvelope
}
],
Annotations = annotations
};
var result = await _pusher.PushAsync(pushRequest, cancellationToken).ConfigureAwait(false);
stopwatch.Stop();
if (result.Success)
{
// Record success metrics
VerdictPushDiagnostics.RecordPushSuccess(
registry,
request.Decision,
stopwatch.Elapsed.TotalMilliseconds,
payloadSize);
VerdictPushDiagnostics.SetActivitySuccess(activity, result.ManifestDigest);
}
else
{
// Record failure metrics
VerdictPushDiagnostics.RecordPushFailure(
registry,
request.Decision,
result.Error ?? "unknown",
stopwatch.Elapsed.TotalMilliseconds);
activity?.SetStatus(ActivityStatusCode.Error, result.Error);
}
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
// Record failure metrics
VerdictPushDiagnostics.RecordPushFailure(
registry,
request.Decision,
ex.GetType().Name,
stopwatch.Elapsed.TotalMilliseconds);
VerdictPushDiagnostics.SetActivityError(activity, ex);
throw;
}
}
/// <summary>
/// Extract registry hostname from an OCI reference.
/// </summary>
private static string ExtractRegistry(string reference)
{
if (string.IsNullOrWhiteSpace(reference))
{
return "unknown";
}
// Remove tag or digest suffix
var refWithoutTag = reference;
var atIndex = reference.IndexOf('@');
if (atIndex > 0)
{
refWithoutTag = reference[..atIndex];
}
else
{
var colonIndex = reference.LastIndexOf(':');
if (colonIndex > 0)
{
// Check if it's a port number or tag
var slashIndex = reference.LastIndexOf('/');
if (slashIndex < colonIndex)
{
refWithoutTag = reference[..colonIndex];
}
}
}
// Extract registry (first path component)
var firstSlash = refWithoutTag.IndexOf('/');
if (firstSlash > 0)
{
var potentialRegistry = refWithoutTag[..firstSlash];
// Check if it looks like a registry (contains . or : or is localhost)
if (potentialRegistry.Contains('.') ||
potentialRegistry.Contains(':') ||
potentialRegistry.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return potentialRegistry;
}
}
// Default to docker.io for implicit registry
return "docker.io";
}
}
/// <summary>
/// Predicate type URIs for verdict attestations.
/// </summary>
public static class VerdictPredicateTypes
{
/// <summary>
/// Predicate type for risk verdict attestations.
/// </summary>
public const string Verdict = "verdict.stella/v1";
}