up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

View File

@@ -1,24 +1,24 @@
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Contract implemented by language ecosystem analyzers. Analyzers must be deterministic,
/// cancellation-aware, and refrain from mutating shared state.
/// </summary>
public interface ILanguageAnalyzer
{
/// <summary>
/// Stable identifier (e.g., <c>java</c>, <c>node</c>).
/// </summary>
string Id { get; }
/// <summary>
/// Human-readable display name for diagnostics.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Executes the analyzer against the resolved filesystem.
/// </summary>
ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken);
}
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Contract implemented by language ecosystem analyzers. Analyzers must be deterministic,
/// cancellation-aware, and refrain from mutating shared state.
/// </summary>
public interface ILanguageAnalyzer
{
/// <summary>
/// Stable identifier (e.g., <c>java</c>, <c>node</c>).
/// </summary>
string Id { get; }
/// <summary>
/// Human-readable display name for diagnostics.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Executes the analyzer against the resolved filesystem.
/// </summary>
ValueTask AnalyzeAsync(LanguageAnalyzerContext context, LanguageComponentWriter writer, CancellationToken cancellationToken);
}

View File

@@ -1,16 +1,16 @@
namespace StellaOps.Scanner.Analyzers.Lang.Internal;
internal static class LanguageAnalyzerJson
{
public static JsonSerializerOptions CreateDefault(bool indent = false)
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = indent,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
return options;
}
}
namespace StellaOps.Scanner.Analyzers.Lang.Internal;
internal static class LanguageAnalyzerJson
{
public static JsonSerializerOptions CreateDefault(bool indent = false)
{
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = indent,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
return options;
}
}

View File

@@ -19,13 +19,13 @@ public sealed class LanguageAnalyzerContext
{
throw new ArgumentException("Root path is required", nameof(rootPath));
}
RootPath = Path.GetFullPath(rootPath);
if (!Directory.Exists(RootPath))
{
throw new DirectoryNotFoundException($"Root path '{RootPath}' does not exist.");
}
RootPath = Path.GetFullPath(rootPath);
if (!Directory.Exists(RootPath))
{
throw new DirectoryNotFoundException($"Root path '{RootPath}' does not exist.");
}
TimeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
UsageHints = usageHints ?? LanguageUsageHints.Empty;
Services = services;
@@ -50,35 +50,35 @@ public sealed class LanguageAnalyzerContext
if (Services is null)
{
service = null;
return false;
}
service = Services.GetService(typeof(T)) as T;
return service is not null;
}
public string ResolvePath(ReadOnlySpan<char> relative)
{
if (relative.IsEmpty)
{
return RootPath;
}
var relativeString = new string(relative);
return false;
}
service = Services.GetService(typeof(T)) as T;
return service is not null;
}
public string ResolvePath(ReadOnlySpan<char> relative)
{
if (relative.IsEmpty)
{
return RootPath;
}
var relativeString = new string(relative);
var combined = Path.Combine(RootPath, relativeString);
return Path.GetFullPath(combined);
}
public string GetRelativePath(string absolutePath)
{
if (string.IsNullOrWhiteSpace(absolutePath))
{
return string.Empty;
}
var relative = Path.GetRelativePath(RootPath, absolutePath);
return OperatingSystem.IsWindows()
? relative.Replace('\\', '/')
{
if (string.IsNullOrWhiteSpace(absolutePath))
{
return string.Empty;
}
var relative = Path.GetRelativePath(RootPath, absolutePath);
return OperatingSystem.IsWindows()
? relative.Replace('\\', '/')
: relative;
}

View File

@@ -1,59 +1,59 @@
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageAnalyzerEngine
{
private readonly IReadOnlyList<ILanguageAnalyzer> _analyzers;
public LanguageAnalyzerEngine(IEnumerable<ILanguageAnalyzer> analyzers)
{
if (analyzers is null)
{
throw new ArgumentNullException(nameof(analyzers));
}
_analyzers = analyzers
.Where(static analyzer => analyzer is not null)
.Distinct(new AnalyzerIdComparer())
.OrderBy(static analyzer => analyzer.Id, StringComparer.Ordinal)
.ToArray();
}
public IReadOnlyList<ILanguageAnalyzer> Analyzers => _analyzers;
public async ValueTask<LanguageAnalyzerResult> AnalyzeAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
var builder = new LanguageAnalyzerResultBuilder();
var writer = new LanguageComponentWriter(builder);
foreach (var analyzer in _analyzers)
{
cancellationToken.ThrowIfCancellationRequested();
await analyzer.AnalyzeAsync(context, writer, cancellationToken).ConfigureAwait(false);
}
return builder.Build();
}
private sealed class AnalyzerIdComparer : IEqualityComparer<ILanguageAnalyzer>
{
public bool Equals(ILanguageAnalyzer? x, ILanguageAnalyzer? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return string.Equals(x.Id, y.Id, StringComparison.Ordinal);
}
public int GetHashCode(ILanguageAnalyzer obj)
=> obj?.Id is null ? 0 : StringComparer.Ordinal.GetHashCode(obj.Id);
}
}
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageAnalyzerEngine
{
private readonly IReadOnlyList<ILanguageAnalyzer> _analyzers;
public LanguageAnalyzerEngine(IEnumerable<ILanguageAnalyzer> analyzers)
{
if (analyzers is null)
{
throw new ArgumentNullException(nameof(analyzers));
}
_analyzers = analyzers
.Where(static analyzer => analyzer is not null)
.Distinct(new AnalyzerIdComparer())
.OrderBy(static analyzer => analyzer.Id, StringComparer.Ordinal)
.ToArray();
}
public IReadOnlyList<ILanguageAnalyzer> Analyzers => _analyzers;
public async ValueTask<LanguageAnalyzerResult> AnalyzeAsync(LanguageAnalyzerContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
var builder = new LanguageAnalyzerResultBuilder();
var writer = new LanguageComponentWriter(builder);
foreach (var analyzer in _analyzers)
{
cancellationToken.ThrowIfCancellationRequested();
await analyzer.AnalyzeAsync(context, writer, cancellationToken).ConfigureAwait(false);
}
return builder.Build();
}
private sealed class AnalyzerIdComparer : IEqualityComparer<ILanguageAnalyzer>
{
public bool Equals(ILanguageAnalyzer? x, ILanguageAnalyzer? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (x is null || y is null)
{
return false;
}
return string.Equals(x.Id, y.Id, StringComparison.Ordinal);
}
public int GetHashCode(ILanguageAnalyzer obj)
=> obj?.Id is null ? 0 : StringComparer.Ordinal.GetHashCode(obj.Id);
}
}

View File

@@ -1,27 +1,27 @@
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageAnalyzerResult
{
private readonly ImmutableArray<LanguageComponentRecord> _components;
internal LanguageAnalyzerResult(IEnumerable<LanguageComponentRecord> components)
{
_components = components
.OrderBy(static record => record.ComponentKey, StringComparer.Ordinal)
.ThenBy(static record => record.AnalyzerId, StringComparer.Ordinal)
.ToImmutableArray();
}
public IReadOnlyList<LanguageComponentRecord> Components => _components;
public ImmutableArray<ComponentRecord> ToComponentRecords(string analyzerId, string? layerDigest = null)
=> LanguageComponentMapper.ToComponentRecords(analyzerId, _components, layerDigest);
public LayerComponentFragment ToLayerFragment(string analyzerId, string? layerDigest = null)
=> LanguageComponentMapper.ToLayerFragment(analyzerId, _components, layerDigest);
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageAnalyzerResult
{
private readonly ImmutableArray<LanguageComponentRecord> _components;
internal LanguageAnalyzerResult(IEnumerable<LanguageComponentRecord> components)
{
_components = components
.OrderBy(static record => record.ComponentKey, StringComparer.Ordinal)
.ThenBy(static record => record.AnalyzerId, StringComparer.Ordinal)
.ToImmutableArray();
}
public IReadOnlyList<LanguageComponentRecord> Components => _components;
public ImmutableArray<ComponentRecord> ToComponentRecords(string analyzerId, string? layerDigest = null)
=> LanguageComponentMapper.ToComponentRecords(analyzerId, _components, layerDigest);
public LayerComponentFragment ToLayerFragment(string analyzerId, string? layerDigest = null)
=> LanguageComponentMapper.ToLayerFragment(analyzerId, _components, layerDigest);
public IReadOnlyList<LanguageComponentSnapshot> ToSnapshots()
=> _components.Select(static component => component.ToSnapshot()).ToImmutableArray();
@@ -41,82 +41,82 @@ public sealed class LanguageAnalyzerResult
var snapshots = ToSnapshots();
var options = Internal.LanguageAnalyzerJson.CreateDefault(indent);
return JsonSerializer.Serialize(snapshots, options);
}
}
internal sealed class LanguageAnalyzerResultBuilder
{
private readonly Dictionary<string, LanguageComponentRecord> _records = new(StringComparer.Ordinal);
private readonly object _sync = new();
public void Add(LanguageComponentRecord record)
{
ArgumentNullException.ThrowIfNull(record);
lock (_sync)
{
if (_records.TryGetValue(record.ComponentKey, out var existing))
{
existing.Merge(record);
return;
}
_records[record.ComponentKey] = record;
}
}
public void AddRange(IEnumerable<LanguageComponentRecord> records)
{
foreach (var record in records ?? Array.Empty<LanguageComponentRecord>())
{
Add(record);
}
}
public LanguageAnalyzerResult Build()
{
lock (_sync)
{
return new LanguageAnalyzerResult(_records.Values.ToArray());
}
}
}
public sealed class LanguageComponentWriter
{
private readonly LanguageAnalyzerResultBuilder _builder;
internal LanguageComponentWriter(LanguageAnalyzerResultBuilder builder)
{
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
}
public void Add(LanguageComponentRecord record)
=> _builder.Add(record);
public void AddRange(IEnumerable<LanguageComponentRecord> records)
=> _builder.AddRange(records);
public void AddFromPurl(
string analyzerId,
string purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
=> Add(LanguageComponentRecord.FromPurl(analyzerId, purl, name, version, type, metadata, evidence, usedByEntrypoint));
public void AddFromExplicitKey(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
=> Add(LanguageComponentRecord.FromExplicitKey(analyzerId, componentKey, purl, name, version, type, metadata, evidence, usedByEntrypoint));
}
}
}
internal sealed class LanguageAnalyzerResultBuilder
{
private readonly Dictionary<string, LanguageComponentRecord> _records = new(StringComparer.Ordinal);
private readonly object _sync = new();
public void Add(LanguageComponentRecord record)
{
ArgumentNullException.ThrowIfNull(record);
lock (_sync)
{
if (_records.TryGetValue(record.ComponentKey, out var existing))
{
existing.Merge(record);
return;
}
_records[record.ComponentKey] = record;
}
}
public void AddRange(IEnumerable<LanguageComponentRecord> records)
{
foreach (var record in records ?? Array.Empty<LanguageComponentRecord>())
{
Add(record);
}
}
public LanguageAnalyzerResult Build()
{
lock (_sync)
{
return new LanguageAnalyzerResult(_records.Values.ToArray());
}
}
}
public sealed class LanguageComponentWriter
{
private readonly LanguageAnalyzerResultBuilder _builder;
internal LanguageComponentWriter(LanguageAnalyzerResultBuilder builder)
{
_builder = builder ?? throw new ArgumentNullException(nameof(builder));
}
public void Add(LanguageComponentRecord record)
=> _builder.Add(record);
public void AddRange(IEnumerable<LanguageComponentRecord> records)
=> _builder.AddRange(records);
public void AddFromPurl(
string analyzerId,
string purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
=> Add(LanguageComponentRecord.FromPurl(analyzerId, purl, name, version, type, metadata, evidence, usedByEntrypoint));
public void AddFromExplicitKey(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
=> Add(LanguageComponentRecord.FromExplicitKey(analyzerId, componentKey, purl, name, version, type, metadata, evidence, usedByEntrypoint));
}

View File

@@ -1,18 +1,18 @@
namespace StellaOps.Scanner.Analyzers.Lang;
public enum LanguageEvidenceKind
{
File,
Metadata,
Derived,
}
public sealed record LanguageComponentEvidence(
LanguageEvidenceKind Kind,
string Source,
string Locator,
string? Value,
string? Sha256)
{
public string ComparisonKey => string.Join('|', Kind, Source, Locator, Value, Sha256);
}
namespace StellaOps.Scanner.Analyzers.Lang;
public enum LanguageEvidenceKind
{
File,
Metadata,
Derived,
}
public sealed record LanguageComponentEvidence(
LanguageEvidenceKind Kind,
string Source,
string Locator,
string? Value,
string? Sha256)
{
public string ComparisonKey => string.Join('|', Kind, Source, Locator, Value, Sha256);
}

View File

@@ -1,223 +1,223 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Helpers converting language analyzer component records into canonical scanner component models.
/// </summary>
public static class LanguageComponentMapper
{
private const string LayerHashPrefix = "stellaops:lang:";
private const string MetadataPrefix = "stellaops.lang";
/// <summary>
/// Computes a deterministic synthetic layer digest for the supplied analyzer identifier.
/// </summary>
public static string ComputeLayerDigest(string analyzerId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(analyzerId);
var payload = $"{LayerHashPrefix}{analyzerId.Trim().ToLowerInvariant()}";
var bytes = Encoding.UTF8.GetBytes(payload);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Projects language component records into a deterministic set of component records.
/// </summary>
public static ImmutableArray<ComponentRecord> ToComponentRecords(
string analyzerId,
IEnumerable<LanguageComponentRecord> components,
string? layerDigest = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(analyzerId);
ArgumentNullException.ThrowIfNull(components);
var effectiveLayer = string.IsNullOrWhiteSpace(layerDigest)
? ComputeLayerDigest(analyzerId)
: layerDigest!;
var builder = ImmutableArray.CreateBuilder<ComponentRecord>();
foreach (var record in components.OrderBy(static component => component.ComponentKey, StringComparer.Ordinal))
{
builder.Add(CreateComponentRecord(analyzerId, effectiveLayer, record));
}
return builder.ToImmutable();
}
/// <summary>
/// Creates a layer component fragment using the supplied component records.
/// </summary>
public static LayerComponentFragment ToLayerFragment(
string analyzerId,
IEnumerable<LanguageComponentRecord> components,
string? layerDigest = null)
{
var componentRecords = ToComponentRecords(analyzerId, components, layerDigest);
if (componentRecords.IsEmpty)
{
return LayerComponentFragment.Create(ComputeLayerDigest(analyzerId), componentRecords);
}
return LayerComponentFragment.Create(componentRecords[0].LayerDigest, componentRecords);
}
private static ComponentRecord CreateComponentRecord(
string analyzerId,
string layerDigest,
LanguageComponentRecord record)
{
ArgumentNullException.ThrowIfNull(record);
var identity = ComponentIdentity.Create(
key: ResolveIdentityKey(record),
name: record.Name,
version: record.Version,
purl: record.Purl,
componentType: record.Type);
var evidence = MapEvidence(record);
var metadata = BuildMetadata(analyzerId, record);
var usage = record.UsedByEntrypoint
? ComponentUsage.Create(usedByEntrypoint: true)
: ComponentUsage.Unused;
return new ComponentRecord
{
Identity = identity,
LayerDigest = layerDigest,
Evidence = evidence,
Dependencies = ImmutableArray<string>.Empty,
Metadata = metadata,
Usage = usage,
};
}
private static ImmutableArray<ComponentEvidence> MapEvidence(LanguageComponentRecord record)
{
var builder = ImmutableArray.CreateBuilder<ComponentEvidence>();
foreach (var item in record.Evidence)
{
if (item is null)
{
continue;
}
var kind = item.Kind switch
{
LanguageEvidenceKind.File => "file",
LanguageEvidenceKind.Metadata => "metadata",
LanguageEvidenceKind.Derived => "derived",
_ => "unknown",
};
var value = string.IsNullOrWhiteSpace(item.Locator) ? item.Source : item.Locator;
if (string.IsNullOrWhiteSpace(value))
{
value = kind;
}
builder.Add(new ComponentEvidence
{
Kind = kind,
Value = value,
Source = string.IsNullOrWhiteSpace(item.Source) ? null : item.Source,
});
}
return builder.Count == 0
? ImmutableArray<ComponentEvidence>.Empty
: builder.ToImmutable();
}
private static ComponentMetadata? BuildMetadata(string analyzerId, LanguageComponentRecord record)
{
var properties = new SortedDictionary<string, string>(StringComparer.Ordinal)
{
[$"{MetadataPrefix}.analyzerId"] = analyzerId
};
var licenseList = new List<string>();
foreach (var pair in record.Metadata)
{
if (string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
if (!string.IsNullOrWhiteSpace(pair.Value))
{
var value = pair.Value.Trim();
properties[$"{MetadataPrefix}.meta.{pair.Key}"] = value;
if (IsLicenseKey(pair.Key) && value.Length > 0)
{
foreach (var candidate in value.Split(new[] { ',', ';' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
if (candidate.Length > 0)
{
licenseList.Add(candidate);
}
}
}
}
}
var evidenceIndex = 0;
foreach (var evidence in record.Evidence)
{
if (evidence is null)
{
continue;
}
var prefix = $"{MetadataPrefix}.evidence.{evidenceIndex}";
if (!string.IsNullOrWhiteSpace(evidence.Value))
{
properties[$"{prefix}.value"] = evidence.Value.Trim();
}
if (!string.IsNullOrWhiteSpace(evidence.Sha256))
{
properties[$"{prefix}.sha256"] = evidence.Sha256.Trim();
}
evidenceIndex++;
}
IReadOnlyList<string>? licenses = null;
if (licenseList.Count > 0)
{
licenses = licenseList
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(static license => license, StringComparer.Ordinal)
.ToArray();
}
return new ComponentMetadata
{
Licenses = licenses,
Properties = properties.Count == 0 ? null : properties,
};
}
private static string ResolveIdentityKey(LanguageComponentRecord record)
{
var key = record.ComponentKey;
if (key.StartsWith("purl::", StringComparison.Ordinal))
{
return key[6..];
}
return key;
}
private static bool IsLicenseKey(string key)
=> key.Contains("license", StringComparison.OrdinalIgnoreCase);
}
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Scanner.Core.Contracts;
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Helpers converting language analyzer component records into canonical scanner component models.
/// </summary>
public static class LanguageComponentMapper
{
private const string LayerHashPrefix = "stellaops:lang:";
private const string MetadataPrefix = "stellaops.lang";
/// <summary>
/// Computes a deterministic synthetic layer digest for the supplied analyzer identifier.
/// </summary>
public static string ComputeLayerDigest(string analyzerId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(analyzerId);
var payload = $"{LayerHashPrefix}{analyzerId.Trim().ToLowerInvariant()}";
var bytes = Encoding.UTF8.GetBytes(payload);
var hash = SHA256.HashData(bytes);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
/// <summary>
/// Projects language component records into a deterministic set of component records.
/// </summary>
public static ImmutableArray<ComponentRecord> ToComponentRecords(
string analyzerId,
IEnumerable<LanguageComponentRecord> components,
string? layerDigest = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(analyzerId);
ArgumentNullException.ThrowIfNull(components);
var effectiveLayer = string.IsNullOrWhiteSpace(layerDigest)
? ComputeLayerDigest(analyzerId)
: layerDigest!;
var builder = ImmutableArray.CreateBuilder<ComponentRecord>();
foreach (var record in components.OrderBy(static component => component.ComponentKey, StringComparer.Ordinal))
{
builder.Add(CreateComponentRecord(analyzerId, effectiveLayer, record));
}
return builder.ToImmutable();
}
/// <summary>
/// Creates a layer component fragment using the supplied component records.
/// </summary>
public static LayerComponentFragment ToLayerFragment(
string analyzerId,
IEnumerable<LanguageComponentRecord> components,
string? layerDigest = null)
{
var componentRecords = ToComponentRecords(analyzerId, components, layerDigest);
if (componentRecords.IsEmpty)
{
return LayerComponentFragment.Create(ComputeLayerDigest(analyzerId), componentRecords);
}
return LayerComponentFragment.Create(componentRecords[0].LayerDigest, componentRecords);
}
private static ComponentRecord CreateComponentRecord(
string analyzerId,
string layerDigest,
LanguageComponentRecord record)
{
ArgumentNullException.ThrowIfNull(record);
var identity = ComponentIdentity.Create(
key: ResolveIdentityKey(record),
name: record.Name,
version: record.Version,
purl: record.Purl,
componentType: record.Type);
var evidence = MapEvidence(record);
var metadata = BuildMetadata(analyzerId, record);
var usage = record.UsedByEntrypoint
? ComponentUsage.Create(usedByEntrypoint: true)
: ComponentUsage.Unused;
return new ComponentRecord
{
Identity = identity,
LayerDigest = layerDigest,
Evidence = evidence,
Dependencies = ImmutableArray<string>.Empty,
Metadata = metadata,
Usage = usage,
};
}
private static ImmutableArray<ComponentEvidence> MapEvidence(LanguageComponentRecord record)
{
var builder = ImmutableArray.CreateBuilder<ComponentEvidence>();
foreach (var item in record.Evidence)
{
if (item is null)
{
continue;
}
var kind = item.Kind switch
{
LanguageEvidenceKind.File => "file",
LanguageEvidenceKind.Metadata => "metadata",
LanguageEvidenceKind.Derived => "derived",
_ => "unknown",
};
var value = string.IsNullOrWhiteSpace(item.Locator) ? item.Source : item.Locator;
if (string.IsNullOrWhiteSpace(value))
{
value = kind;
}
builder.Add(new ComponentEvidence
{
Kind = kind,
Value = value,
Source = string.IsNullOrWhiteSpace(item.Source) ? null : item.Source,
});
}
return builder.Count == 0
? ImmutableArray<ComponentEvidence>.Empty
: builder.ToImmutable();
}
private static ComponentMetadata? BuildMetadata(string analyzerId, LanguageComponentRecord record)
{
var properties = new SortedDictionary<string, string>(StringComparer.Ordinal)
{
[$"{MetadataPrefix}.analyzerId"] = analyzerId
};
var licenseList = new List<string>();
foreach (var pair in record.Metadata)
{
if (string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
if (!string.IsNullOrWhiteSpace(pair.Value))
{
var value = pair.Value.Trim();
properties[$"{MetadataPrefix}.meta.{pair.Key}"] = value;
if (IsLicenseKey(pair.Key) && value.Length > 0)
{
foreach (var candidate in value.Split(new[] { ',', ';' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
{
if (candidate.Length > 0)
{
licenseList.Add(candidate);
}
}
}
}
}
var evidenceIndex = 0;
foreach (var evidence in record.Evidence)
{
if (evidence is null)
{
continue;
}
var prefix = $"{MetadataPrefix}.evidence.{evidenceIndex}";
if (!string.IsNullOrWhiteSpace(evidence.Value))
{
properties[$"{prefix}.value"] = evidence.Value.Trim();
}
if (!string.IsNullOrWhiteSpace(evidence.Sha256))
{
properties[$"{prefix}.sha256"] = evidence.Sha256.Trim();
}
evidenceIndex++;
}
IReadOnlyList<string>? licenses = null;
if (licenseList.Count > 0)
{
licenses = licenseList
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(static license => license, StringComparer.Ordinal)
.ToArray();
}
return new ComponentMetadata
{
Licenses = licenses,
Properties = properties.Count == 0 ? null : properties,
};
}
private static string ResolveIdentityKey(LanguageComponentRecord record)
{
var key = record.ComponentKey;
if (key.StartsWith("purl::", StringComparison.Ordinal))
{
return key[6..];
}
return key;
}
private static bool IsLicenseKey(string key)
=> key.Contains("license", StringComparison.OrdinalIgnoreCase);
}

View File

@@ -1,114 +1,114 @@
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageComponentRecord
{
private readonly SortedDictionary<string, string?> _metadata;
private readonly SortedDictionary<string, LanguageComponentEvidence> _evidence;
private LanguageComponentRecord(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>> metadata,
IEnumerable<LanguageComponentEvidence> evidence,
bool usedByEntrypoint)
{
AnalyzerId = analyzerId ?? throw new ArgumentNullException(nameof(analyzerId));
ComponentKey = componentKey ?? throw new ArgumentNullException(nameof(componentKey));
Purl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim();
Name = name ?? throw new ArgumentNullException(nameof(name));
Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim();
Type = string.IsNullOrWhiteSpace(type) ? throw new ArgumentException("Type is required", nameof(type)) : type.Trim();
UsedByEntrypoint = usedByEntrypoint;
_metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal);
foreach (var entry in metadata ?? Array.Empty<KeyValuePair<string, string?>>())
{
if (string.IsNullOrWhiteSpace(entry.Key))
{
continue;
}
_metadata[entry.Key.Trim()] = entry.Value;
}
_evidence = new SortedDictionary<string, LanguageComponentEvidence>(StringComparer.Ordinal);
foreach (var evidenceItem in evidence ?? Array.Empty<LanguageComponentEvidence>())
{
if (evidenceItem is null)
{
continue;
}
_evidence[evidenceItem.ComparisonKey] = evidenceItem;
}
}
public string AnalyzerId { get; }
public string ComponentKey { get; }
public string? Purl { get; }
public string Name { get; }
public string? Version { get; }
public string Type { get; }
public bool UsedByEntrypoint { get; private set; }
public IReadOnlyDictionary<string, string?> Metadata => _metadata;
public IReadOnlyCollection<LanguageComponentEvidence> Evidence => _evidence.Values;
public static LanguageComponentRecord FromPurl(
string analyzerId,
string purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
{
if (string.IsNullOrWhiteSpace(purl))
{
throw new ArgumentException("purl is required", nameof(purl));
}
var key = $"purl::{purl.Trim()}";
return new LanguageComponentRecord(
analyzerId,
key,
purl,
name,
version,
type,
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
evidence ?? Array.Empty<LanguageComponentEvidence>(),
usedByEntrypoint);
}
public static LanguageComponentRecord FromExplicitKey(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
{
if (string.IsNullOrWhiteSpace(componentKey))
{
throw new ArgumentException("Component key is required", nameof(componentKey));
}
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageComponentRecord
{
private readonly SortedDictionary<string, string?> _metadata;
private readonly SortedDictionary<string, LanguageComponentEvidence> _evidence;
private LanguageComponentRecord(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>> metadata,
IEnumerable<LanguageComponentEvidence> evidence,
bool usedByEntrypoint)
{
AnalyzerId = analyzerId ?? throw new ArgumentNullException(nameof(analyzerId));
ComponentKey = componentKey ?? throw new ArgumentNullException(nameof(componentKey));
Purl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim();
Name = name ?? throw new ArgumentNullException(nameof(name));
Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim();
Type = string.IsNullOrWhiteSpace(type) ? throw new ArgumentException("Type is required", nameof(type)) : type.Trim();
UsedByEntrypoint = usedByEntrypoint;
_metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal);
foreach (var entry in metadata ?? Array.Empty<KeyValuePair<string, string?>>())
{
if (string.IsNullOrWhiteSpace(entry.Key))
{
continue;
}
_metadata[entry.Key.Trim()] = entry.Value;
}
_evidence = new SortedDictionary<string, LanguageComponentEvidence>(StringComparer.Ordinal);
foreach (var evidenceItem in evidence ?? Array.Empty<LanguageComponentEvidence>())
{
if (evidenceItem is null)
{
continue;
}
_evidence[evidenceItem.ComparisonKey] = evidenceItem;
}
}
public string AnalyzerId { get; }
public string ComponentKey { get; }
public string? Purl { get; }
public string Name { get; }
public string? Version { get; }
public string Type { get; }
public bool UsedByEntrypoint { get; private set; }
public IReadOnlyDictionary<string, string?> Metadata => _metadata;
public IReadOnlyCollection<LanguageComponentEvidence> Evidence => _evidence.Values;
public static LanguageComponentRecord FromPurl(
string analyzerId,
string purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
{
if (string.IsNullOrWhiteSpace(purl))
{
throw new ArgumentException("purl is required", nameof(purl));
}
var key = $"purl::{purl.Trim()}";
return new LanguageComponentRecord(
analyzerId,
key,
purl,
name,
version,
type,
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
evidence ?? Array.Empty<LanguageComponentEvidence>(),
usedByEntrypoint);
}
public static LanguageComponentRecord FromExplicitKey(
string analyzerId,
string componentKey,
string? purl,
string name,
string? version,
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
{
if (string.IsNullOrWhiteSpace(componentKey))
{
throw new ArgumentException("Component key is required", nameof(componentKey));
}
return new LanguageComponentRecord(
analyzerId,
componentKey.Trim(),
@@ -174,94 +174,94 @@ public sealed class LanguageComponentRecord
ArgumentNullException.ThrowIfNull(other);
if (!ComponentKey.Equals(other.ComponentKey, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Cannot merge component '{ComponentKey}' with '{other.ComponentKey}'.");
}
UsedByEntrypoint |= other.UsedByEntrypoint;
foreach (var entry in other._metadata)
{
if (!_metadata.TryGetValue(entry.Key, out var existing) || string.IsNullOrEmpty(existing))
{
_metadata[entry.Key] = entry.Value;
}
}
foreach (var evidenceItem in other._evidence)
{
_evidence[evidenceItem.Key] = evidenceItem.Value;
}
}
public LanguageComponentSnapshot ToSnapshot()
{
return new LanguageComponentSnapshot
{
AnalyzerId = AnalyzerId,
ComponentKey = ComponentKey,
Purl = Purl,
Name = Name,
Version = Version,
Type = Type,
UsedByEntrypoint = UsedByEntrypoint,
Metadata = _metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
Evidence = _evidence.Values.Select(static item => new LanguageComponentEvidenceSnapshot
{
Kind = item.Kind,
Source = item.Source,
Locator = item.Locator,
Value = item.Value,
Sha256 = item.Sha256,
}).ToArray(),
};
}
}
public sealed class LanguageComponentSnapshot
{
[JsonPropertyName("analyzerId")]
public string AnalyzerId { get; set; } = string.Empty;
[JsonPropertyName("componentKey")]
public string ComponentKey { get; set; } = string.Empty;
[JsonPropertyName("purl")]
public string? Purl { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("version")]
public string? Version { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("usedByEntrypoint")]
public bool UsedByEntrypoint { get; set; }
[JsonPropertyName("metadata")]
public IDictionary<string, string?> Metadata { get; set; } = new Dictionary<string, string?>(StringComparer.Ordinal);
[JsonPropertyName("evidence")]
public IReadOnlyList<LanguageComponentEvidenceSnapshot> Evidence { get; set; } = Array.Empty<LanguageComponentEvidenceSnapshot>();
}
public sealed class LanguageComponentEvidenceSnapshot
{
[JsonPropertyName("kind")]
public LanguageEvidenceKind Kind { get; set; }
[JsonPropertyName("source")]
public string Source { get; set; } = string.Empty;
[JsonPropertyName("locator")]
public string Locator { get; set; } = string.Empty;
[JsonPropertyName("value")]
public string? Value { get; set; }
[JsonPropertyName("sha256")]
public string? Sha256 { get; set; }
}
{
throw new InvalidOperationException($"Cannot merge component '{ComponentKey}' with '{other.ComponentKey}'.");
}
UsedByEntrypoint |= other.UsedByEntrypoint;
foreach (var entry in other._metadata)
{
if (!_metadata.TryGetValue(entry.Key, out var existing) || string.IsNullOrEmpty(existing))
{
_metadata[entry.Key] = entry.Value;
}
}
foreach (var evidenceItem in other._evidence)
{
_evidence[evidenceItem.Key] = evidenceItem.Value;
}
}
public LanguageComponentSnapshot ToSnapshot()
{
return new LanguageComponentSnapshot
{
AnalyzerId = AnalyzerId,
ComponentKey = ComponentKey,
Purl = Purl,
Name = Name,
Version = Version,
Type = Type,
UsedByEntrypoint = UsedByEntrypoint,
Metadata = _metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
Evidence = _evidence.Values.Select(static item => new LanguageComponentEvidenceSnapshot
{
Kind = item.Kind,
Source = item.Source,
Locator = item.Locator,
Value = item.Value,
Sha256 = item.Sha256,
}).ToArray(),
};
}
}
public sealed class LanguageComponentSnapshot
{
[JsonPropertyName("analyzerId")]
public string AnalyzerId { get; set; } = string.Empty;
[JsonPropertyName("componentKey")]
public string ComponentKey { get; set; } = string.Empty;
[JsonPropertyName("purl")]
public string? Purl { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("version")]
public string? Version { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("usedByEntrypoint")]
public bool UsedByEntrypoint { get; set; }
[JsonPropertyName("metadata")]
public IDictionary<string, string?> Metadata { get; set; } = new Dictionary<string, string?>(StringComparer.Ordinal);
[JsonPropertyName("evidence")]
public IReadOnlyList<LanguageComponentEvidenceSnapshot> Evidence { get; set; } = Array.Empty<LanguageComponentEvidenceSnapshot>();
}
public sealed class LanguageComponentEvidenceSnapshot
{
[JsonPropertyName("kind")]
public LanguageEvidenceKind Kind { get; set; }
[JsonPropertyName("source")]
public string Source { get; set; } = string.Empty;
[JsonPropertyName("locator")]
public string Locator { get; set; } = string.Empty;
[JsonPropertyName("value")]
public string? Value { get; set; }
[JsonPropertyName("sha256")]
public string? Sha256 { get; set; }
}

View File

@@ -1,49 +1,49 @@
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageUsageHints
{
private static readonly StringComparer Comparer = OperatingSystem.IsWindows()
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal;
private readonly ImmutableHashSet<string> _usedPaths;
public static LanguageUsageHints Empty { get; } = new(Array.Empty<string>());
public LanguageUsageHints(IEnumerable<string> usedPaths)
{
if (usedPaths is null)
{
throw new ArgumentNullException(nameof(usedPaths));
}
_usedPaths = usedPaths
.Select(Normalize)
.Where(static path => path.Length > 0)
.ToImmutableHashSet(Comparer);
}
public bool IsPathUsed(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
var normalized = Normalize(path);
return _usedPaths.Contains(normalized);
}
private static string Normalize(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
var full = Path.GetFullPath(path);
return OperatingSystem.IsWindows()
? full.Replace('\\', '/').TrimEnd('/')
: full;
}
}
namespace StellaOps.Scanner.Analyzers.Lang;
public sealed class LanguageUsageHints
{
private static readonly StringComparer Comparer = OperatingSystem.IsWindows()
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal;
private readonly ImmutableHashSet<string> _usedPaths;
public static LanguageUsageHints Empty { get; } = new(Array.Empty<string>());
public LanguageUsageHints(IEnumerable<string> usedPaths)
{
if (usedPaths is null)
{
throw new ArgumentNullException(nameof(usedPaths));
}
_usedPaths = usedPaths
.Select(Normalize)
.Where(static path => path.Length > 0)
.ToImmutableHashSet(Comparer);
}
public bool IsPathUsed(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return false;
}
var normalized = Normalize(path);
return _usedPaths.Contains(normalized);
}
private static string Normalize(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
var full = Path.GetFullPath(path);
return OperatingSystem.IsWindows()
? full.Replace('\\', '/').TrimEnd('/')
: full;
}
}

View File

@@ -1,12 +1,12 @@
global using System;
global using System.Collections.Concurrent;
global using System.Collections.Generic;
global using System.Collections.Immutable;
global using System.Diagnostics.CodeAnalysis;
global using System.Globalization;
global using System.IO;
global using System.Linq;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Threading;
global using System.Threading.Tasks;
global using System;
global using System.Collections.Concurrent;
global using System.Collections.Generic;
global using System.Collections.Immutable;
global using System.Diagnostics.CodeAnalysis;
global using System.Globalization;
global using System.IO;
global using System.Linq;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Threading;
global using System.Threading.Tasks;

View File

@@ -1,15 +1,15 @@
using System;
using StellaOps.Plugin;
namespace StellaOps.Scanner.Analyzers.Lang.Plugin;
/// <summary>
/// Represents a restart-time plug-in that exposes a language analyzer.
/// </summary>
public interface ILanguageAnalyzerPlugin : IAvailabilityPlugin
{
/// <summary>
/// Creates the analyzer instance bound to the service provider.
/// </summary>
ILanguageAnalyzer CreateAnalyzer(IServiceProvider services);
}
using System;
using StellaOps.Plugin;
namespace StellaOps.Scanner.Analyzers.Lang.Plugin;
/// <summary>
/// Represents a restart-time plug-in that exposes a language analyzer.
/// </summary>
public interface ILanguageAnalyzerPlugin : IAvailabilityPlugin
{
/// <summary>
/// Creates the analyzer instance bound to the service provider.
/// </summary>
ILanguageAnalyzer CreateAnalyzer(IServiceProvider services);
}

View File

@@ -1,147 +1,147 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using StellaOps.Plugin;
using StellaOps.Plugin.Hosting;
using StellaOps.Scanner.Core.Security;
namespace StellaOps.Scanner.Analyzers.Lang.Plugin;
public interface ILanguageAnalyzerPluginCatalog
{
IReadOnlyCollection<ILanguageAnalyzerPlugin> Plugins { get; }
void LoadFromDirectory(string directory, bool seal = true);
IReadOnlyList<ILanguageAnalyzer> CreateAnalyzers(IServiceProvider services);
}
public sealed class LanguageAnalyzerPluginCatalog : ILanguageAnalyzerPluginCatalog
{
private readonly ILogger<LanguageAnalyzerPluginCatalog> _logger;
private readonly IPluginCatalogGuard _guard;
private readonly ConcurrentDictionary<string, Assembly> _assemblies = new(StringComparer.OrdinalIgnoreCase);
private IReadOnlyList<ILanguageAnalyzerPlugin> _plugins = Array.Empty<ILanguageAnalyzerPlugin>();
public LanguageAnalyzerPluginCatalog(IPluginCatalogGuard guard, ILogger<LanguageAnalyzerPluginCatalog> logger)
{
_guard = guard ?? throw new ArgumentNullException(nameof(guard));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public IReadOnlyCollection<ILanguageAnalyzerPlugin> Plugins => _plugins;
public void LoadFromDirectory(string directory, bool seal = true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
var fullDirectory = Path.GetFullPath(directory);
var options = new PluginHostOptions
{
PluginsDirectory = fullDirectory,
EnsureDirectoryExists = false,
RecursiveSearch = false,
};
options.SearchPatterns.Add("StellaOps.Scanner.Analyzers.*.dll");
var result = PluginHost.LoadPlugins(options, _logger);
if (result.Plugins.Count == 0)
{
_logger.LogWarning("No language analyzer plug-ins discovered under '{Directory}'.", fullDirectory);
}
foreach (var descriptor in result.Plugins)
{
try
{
_guard.EnsureRegistrationAllowed(descriptor.AssemblyPath);
_assemblies[descriptor.AssemblyPath] = descriptor.Assembly;
_logger.LogInformation(
"Registered language analyzer plug-in assembly '{Assembly}' from '{Path}'.",
descriptor.Assembly.FullName,
descriptor.AssemblyPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register language analyzer plug-in '{Path}'.", descriptor.AssemblyPath);
}
}
RefreshPluginList();
if (seal)
{
_guard.Seal();
}
}
public IReadOnlyList<ILanguageAnalyzer> CreateAnalyzers(IServiceProvider services)
{
ArgumentNullException.ThrowIfNull(services);
if (_plugins.Count == 0)
{
_logger.LogWarning("No language analyzer plug-ins available; skipping language analysis.");
return Array.Empty<ILanguageAnalyzer>();
}
var analyzers = new List<ILanguageAnalyzer>(_plugins.Count);
foreach (var plugin in _plugins)
{
if (!IsPluginAvailable(plugin, services))
{
continue;
}
try
{
var analyzer = plugin.CreateAnalyzer(services);
if (analyzer is null)
{
continue;
}
analyzers.Add(analyzer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Language analyzer plug-in '{Plugin}' failed to create analyzer instance.", plugin.Name);
}
}
if (analyzers.Count == 0)
{
_logger.LogWarning("All language analyzer plug-ins were unavailable.");
return Array.Empty<ILanguageAnalyzer>();
}
analyzers.Sort(static (a, b) => string.CompareOrdinal(a.Id, b.Id));
return new ReadOnlyCollection<ILanguageAnalyzer>(analyzers);
}
private void RefreshPluginList()
{
var assemblies = _assemblies.Values.ToArray();
var plugins = PluginLoader.LoadPlugins<ILanguageAnalyzerPlugin>(assemblies);
_plugins = plugins is IReadOnlyList<ILanguageAnalyzerPlugin> list
? list
: new ReadOnlyCollection<ILanguageAnalyzerPlugin>(plugins.ToArray());
}
private static bool IsPluginAvailable(ILanguageAnalyzerPlugin plugin, IServiceProvider services)
{
try
{
return plugin.IsAvailable(services);
}
catch
{
return false;
}
}
}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Logging;
using StellaOps.Plugin;
using StellaOps.Plugin.Hosting;
using StellaOps.Scanner.Core.Security;
namespace StellaOps.Scanner.Analyzers.Lang.Plugin;
public interface ILanguageAnalyzerPluginCatalog
{
IReadOnlyCollection<ILanguageAnalyzerPlugin> Plugins { get; }
void LoadFromDirectory(string directory, bool seal = true);
IReadOnlyList<ILanguageAnalyzer> CreateAnalyzers(IServiceProvider services);
}
public sealed class LanguageAnalyzerPluginCatalog : ILanguageAnalyzerPluginCatalog
{
private readonly ILogger<LanguageAnalyzerPluginCatalog> _logger;
private readonly IPluginCatalogGuard _guard;
private readonly ConcurrentDictionary<string, Assembly> _assemblies = new(StringComparer.OrdinalIgnoreCase);
private IReadOnlyList<ILanguageAnalyzerPlugin> _plugins = Array.Empty<ILanguageAnalyzerPlugin>();
public LanguageAnalyzerPluginCatalog(IPluginCatalogGuard guard, ILogger<LanguageAnalyzerPluginCatalog> logger)
{
_guard = guard ?? throw new ArgumentNullException(nameof(guard));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public IReadOnlyCollection<ILanguageAnalyzerPlugin> Plugins => _plugins;
public void LoadFromDirectory(string directory, bool seal = true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(directory);
var fullDirectory = Path.GetFullPath(directory);
var options = new PluginHostOptions
{
PluginsDirectory = fullDirectory,
EnsureDirectoryExists = false,
RecursiveSearch = false,
};
options.SearchPatterns.Add("StellaOps.Scanner.Analyzers.*.dll");
var result = PluginHost.LoadPlugins(options, _logger);
if (result.Plugins.Count == 0)
{
_logger.LogWarning("No language analyzer plug-ins discovered under '{Directory}'.", fullDirectory);
}
foreach (var descriptor in result.Plugins)
{
try
{
_guard.EnsureRegistrationAllowed(descriptor.AssemblyPath);
_assemblies[descriptor.AssemblyPath] = descriptor.Assembly;
_logger.LogInformation(
"Registered language analyzer plug-in assembly '{Assembly}' from '{Path}'.",
descriptor.Assembly.FullName,
descriptor.AssemblyPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register language analyzer plug-in '{Path}'.", descriptor.AssemblyPath);
}
}
RefreshPluginList();
if (seal)
{
_guard.Seal();
}
}
public IReadOnlyList<ILanguageAnalyzer> CreateAnalyzers(IServiceProvider services)
{
ArgumentNullException.ThrowIfNull(services);
if (_plugins.Count == 0)
{
_logger.LogWarning("No language analyzer plug-ins available; skipping language analysis.");
return Array.Empty<ILanguageAnalyzer>();
}
var analyzers = new List<ILanguageAnalyzer>(_plugins.Count);
foreach (var plugin in _plugins)
{
if (!IsPluginAvailable(plugin, services))
{
continue;
}
try
{
var analyzer = plugin.CreateAnalyzer(services);
if (analyzer is null)
{
continue;
}
analyzers.Add(analyzer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Language analyzer plug-in '{Plugin}' failed to create analyzer instance.", plugin.Name);
}
}
if (analyzers.Count == 0)
{
_logger.LogWarning("All language analyzer plug-ins were unavailable.");
return Array.Empty<ILanguageAnalyzer>();
}
analyzers.Sort(static (a, b) => string.CompareOrdinal(a.Id, b.Id));
return new ReadOnlyCollection<ILanguageAnalyzer>(analyzers);
}
private void RefreshPluginList()
{
var assemblies = _assemblies.Values.ToArray();
var plugins = PluginLoader.LoadPlugins<ILanguageAnalyzerPlugin>(assemblies);
_plugins = plugins is IReadOnlyList<ILanguageAnalyzerPlugin> list
? list
: new ReadOnlyCollection<ILanguageAnalyzerPlugin>(plugins.ToArray());
}
private static bool IsPluginAvailable(ILanguageAnalyzerPlugin plugin, IServiceProvider services)
{
try
{
return plugin.IsAvailable(services);
}
catch
{
return false;
}
}
}