164 lines
5.3 KiB
C#
164 lines
5.3 KiB
C#
// <copyright file="LegacyEvidencePropertyWriter.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Globalization;
|
|
using CycloneDX.Models;
|
|
using StellaOps.Scanner.Core.Contracts;
|
|
|
|
namespace StellaOps.Scanner.Emit.Evidence;
|
|
|
|
/// <summary>
|
|
/// Writes evidence data to legacy CycloneDX property format for backward compatibility.
|
|
/// Sprint: SPRINT_20260107_005_001 Task EV-009
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Migration Support:</para>
|
|
/// <para>
|
|
/// During the migration period from property-based evidence to native CycloneDX 1.7 evidence fields,
|
|
/// this class provides dual-output capability - writing evidence both to native fields and legacy properties.
|
|
/// </para>
|
|
/// <para>Legacy Property Format:</para>
|
|
/// <list type="bullet">
|
|
/// <item>stellaops:evidence[n]:kind - Evidence kind (identity, occurrence, license, callstack)</item>
|
|
/// <item>stellaops:evidence[n]:source - Evidence source/analyzer</item>
|
|
/// <item>stellaops:evidence[n]:value - Evidence value</item>
|
|
/// <item>stellaops:evidence[n]:confidence - Confidence score (0.0-1.0)</item>
|
|
/// <item>stellaops:evidence[n]:methods - Reference to evidence.methods[] (CycloneDX 1.7)</item>
|
|
/// </list>
|
|
/// </remarks>
|
|
public sealed class LegacyEvidencePropertyWriter
|
|
{
|
|
private const string PropertyPrefix = "stellaops:evidence";
|
|
|
|
/// <summary>
|
|
/// Writes component evidence to legacy property format.
|
|
/// </summary>
|
|
/// <param name="component">The CycloneDX component to add properties to.</param>
|
|
/// <param name="evidence">The evidence collection from Scanner core.</param>
|
|
/// <param name="options">Options controlling evidence output.</param>
|
|
public void WriteEvidenceProperties(
|
|
Component component,
|
|
ImmutableArray<ComponentEvidence> evidence,
|
|
LegacyEvidenceOptions options)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(component);
|
|
|
|
if (evidence.IsDefaultOrEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
component.Properties ??= [];
|
|
|
|
int evidenceIndex = 0;
|
|
foreach (var item in evidence)
|
|
{
|
|
WriteEvidenceItem(component.Properties, item, evidenceIndex, options);
|
|
evidenceIndex++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes legacy evidence properties from a component.
|
|
/// </summary>
|
|
/// <param name="component">The CycloneDX component to clean.</param>
|
|
public void RemoveLegacyProperties(Component component)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(component);
|
|
|
|
if (component.Properties == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
component.Properties.RemoveAll(p => p.Name?.StartsWith(PropertyPrefix, StringComparison.Ordinal) == true);
|
|
}
|
|
|
|
private void WriteEvidenceItem(
|
|
List<Property> properties,
|
|
ComponentEvidence evidence,
|
|
int index,
|
|
LegacyEvidenceOptions options)
|
|
{
|
|
var prefix = $"{PropertyPrefix}[{index.ToString(CultureInfo.InvariantCulture)}]";
|
|
|
|
// Kind
|
|
properties.Add(new Property
|
|
{
|
|
Name = $"{prefix}:kind",
|
|
Value = evidence.Kind,
|
|
});
|
|
|
|
// Source
|
|
if (!string.IsNullOrWhiteSpace(evidence.Source))
|
|
{
|
|
properties.Add(new Property
|
|
{
|
|
Name = $"{prefix}:source",
|
|
Value = evidence.Source,
|
|
});
|
|
}
|
|
|
|
// Value
|
|
if (!string.IsNullOrWhiteSpace(evidence.Value))
|
|
{
|
|
properties.Add(new Property
|
|
{
|
|
Name = $"{prefix}:value",
|
|
Value = evidence.Value,
|
|
});
|
|
}
|
|
|
|
// Methods reference (CycloneDX 1.7 interop)
|
|
if (options.IncludeMethodsReference)
|
|
{
|
|
var methodsReference = MapKindToMethodsReference(evidence.Kind);
|
|
if (!string.IsNullOrWhiteSpace(methodsReference))
|
|
{
|
|
properties.Add(new Property
|
|
{
|
|
Name = $"{prefix}:methods",
|
|
Value = methodsReference,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string? MapKindToMethodsReference(string kind)
|
|
{
|
|
return kind.ToLowerInvariant() switch
|
|
{
|
|
"identity" => "evidence.identity",
|
|
"occurrence" => "evidence.occurrences",
|
|
"license" => "evidence.licenses",
|
|
"callstack" => "evidence.callstack",
|
|
"copyright" => "evidence.copyright",
|
|
"hash" => "evidence.identity",
|
|
"manifest" => "evidence.occurrences",
|
|
"signature" => "evidence.identity",
|
|
_ => null,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options for legacy evidence property output.
|
|
/// </summary>
|
|
public sealed class LegacyEvidenceOptions
|
|
{
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether to include references to CycloneDX 1.7 evidence.methods[].
|
|
/// Default is true.
|
|
/// </summary>
|
|
public bool IncludeMethodsReference { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether legacy properties should be written at all.
|
|
/// When false, only native CycloneDX 1.7 evidence fields are used.
|
|
/// Default is true during migration period.
|
|
/// </summary>
|
|
public bool EnableLegacyProperties { get; set; } = true;
|
|
}
|