131 lines
3.9 KiB
C#
131 lines
3.9 KiB
C#
using System.Text.Json;
|
|
|
|
namespace StellaOps.Scanner.Storage.Oci;
|
|
|
|
/// <summary>
|
|
/// Builds OCI manifests for reachability slices.
|
|
/// Sprint: SPRINT_3850_0001_0001
|
|
/// </summary>
|
|
public sealed class SliceOciManifestBuilder
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
WriteIndented = false
|
|
};
|
|
|
|
/// <summary>
|
|
/// Build OCI push request for a slice artifact.
|
|
/// </summary>
|
|
public OciArtifactPushRequest BuildSlicePushRequest(SliceArtifactInput input)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(input);
|
|
ArgumentNullException.ThrowIfNull(input.Slice);
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(input.Reference);
|
|
|
|
var layers = new List<OciLayerContent>
|
|
{
|
|
BuildSliceLayer(input.Slice, input.SliceQuery)
|
|
};
|
|
|
|
if (input.DsseEnvelope is not null)
|
|
{
|
|
layers.Add(BuildDsseLayer(input.DsseEnvelope));
|
|
}
|
|
|
|
var annotations = BuildAnnotations(input.SliceQuery, input.Slice);
|
|
|
|
return new OciArtifactPushRequest
|
|
{
|
|
Reference = input.Reference,
|
|
ArtifactType = OciMediaTypes.SliceArtifact,
|
|
Layers = layers,
|
|
SubjectDigest = input.SubjectImageDigest,
|
|
Annotations = annotations
|
|
};
|
|
}
|
|
|
|
private OciLayerContent BuildSliceLayer(object slice, SliceQueryMetadata? query)
|
|
{
|
|
var sliceJson = JsonSerializer.SerializeToUtf8Bytes(slice, SerializerOptions);
|
|
|
|
var annotations = new Dictionary<string, string>();
|
|
if (query is not null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(query.CveId))
|
|
annotations["org.stellaops.slice.cve"] = query.CveId;
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.Purl))
|
|
annotations["org.stellaops.slice.purl"] = query.Purl;
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.Verdict))
|
|
annotations["org.stellaops.slice.verdict"] = query.Verdict;
|
|
}
|
|
|
|
return new OciLayerContent
|
|
{
|
|
Content = sliceJson,
|
|
MediaType = OciMediaTypes.ReachabilitySlice,
|
|
Annotations = annotations
|
|
};
|
|
}
|
|
|
|
private OciLayerContent BuildDsseLayer(byte[] dsseEnvelope)
|
|
{
|
|
return new OciLayerContent
|
|
{
|
|
Content = dsseEnvelope,
|
|
MediaType = OciMediaTypes.DsseEnvelope,
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["org.stellaops.attestation.type"] = "in-toto/dsse"
|
|
}
|
|
};
|
|
}
|
|
|
|
private Dictionary<string, string> BuildAnnotations(SliceQueryMetadata? query, object slice)
|
|
{
|
|
var annotations = new Dictionary<string, string>
|
|
{
|
|
["org.opencontainers.image.vendor"] = "StellaOps",
|
|
["org.stellaops.artifact.type"] = "reachability-slice"
|
|
};
|
|
|
|
if (query is not null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(query.CveId))
|
|
annotations["org.stellaops.slice.query.cve"] = query.CveId;
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.Purl))
|
|
annotations["org.stellaops.slice.query.purl"] = query.Purl;
|
|
|
|
if (!string.IsNullOrWhiteSpace(query.ScanId))
|
|
annotations["org.stellaops.slice.scan-id"] = query.ScanId;
|
|
}
|
|
|
|
return annotations;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Input for building a slice OCI artifact.
|
|
/// </summary>
|
|
public sealed record SliceArtifactInput
|
|
{
|
|
public required string Reference { get; init; }
|
|
public required object Slice { get; init; }
|
|
public byte[]? DsseEnvelope { get; init; }
|
|
public SliceQueryMetadata? SliceQuery { get; init; }
|
|
public string? SubjectImageDigest { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query metadata for slice annotations.
|
|
/// </summary>
|
|
public sealed record SliceQueryMetadata
|
|
{
|
|
public string? CveId { get; init; }
|
|
public string? Purl { get; init; }
|
|
public string? Verdict { get; init; }
|
|
public string? ScanId { get; init; }
|
|
}
|