//
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
//
using System.Collections.Immutable;
using System.Globalization;
using CycloneDX.Models;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Emit.Evidence;
///
/// Writes evidence data to legacy CycloneDX property format for backward compatibility.
/// Sprint: SPRINT_20260107_005_001 Task EV-009
///
///
/// Migration Support:
///
/// 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.
///
/// Legacy Property Format:
///
/// - stellaops:evidence[n]:kind - Evidence kind (identity, occurrence, license, callstack)
/// - stellaops:evidence[n]:source - Evidence source/analyzer
/// - stellaops:evidence[n]:value - Evidence value
/// - stellaops:evidence[n]:confidence - Confidence score (0.0-1.0)
/// - stellaops:evidence[n]:methods - Reference to evidence.methods[] (CycloneDX 1.7)
///
///
public sealed class LegacyEvidencePropertyWriter
{
private const string PropertyPrefix = "stellaops:evidence";
///
/// Writes component evidence to legacy property format.
///
/// The CycloneDX component to add properties to.
/// The evidence collection from Scanner core.
/// Options controlling evidence output.
public void WriteEvidenceProperties(
Component component,
ImmutableArray 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++;
}
}
///
/// Removes legacy evidence properties from a component.
///
/// The CycloneDX component to clean.
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 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,
};
}
}
///
/// Options for legacy evidence property output.
///
public sealed class LegacyEvidenceOptions
{
///
/// Gets or sets a value indicating whether to include references to CycloneDX 1.7 evidence.methods[].
/// Default is true.
///
public bool IncludeMethodsReference { get; set; } = true;
///
/// 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.
///
public bool EnableLegacyProperties { get; set; } = true;
}