up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (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
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 18:08:55 +02:00
parent 6e45066e37
commit f1a39c4ce3
234 changed files with 24038 additions and 6910 deletions

View File

@@ -1,9 +1,17 @@
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Represents a language component discovered during analysis.
/// </summary>
/// <remarks>
/// Updated in Sprint 0411 - Semantic Entrypoint Engine (Task 18) to include semantic fields.
/// </remarks>
public sealed class LanguageComponentRecord
{
private readonly SortedDictionary<string, string?> _metadata;
private readonly SortedDictionary<string, LanguageComponentEvidence> _evidence;
private readonly List<string> _capabilities;
private readonly List<ComponentThreatVector> _threatVectors;
private LanguageComponentRecord(
string analyzerId,
@@ -14,7 +22,10 @@ public sealed class LanguageComponentRecord
string type,
IEnumerable<KeyValuePair<string, string?>> metadata,
IEnumerable<LanguageComponentEvidence> evidence,
bool usedByEntrypoint)
bool usedByEntrypoint,
string? intent = null,
IEnumerable<string>? capabilities = null,
IEnumerable<ComponentThreatVector>? threatVectors = null)
{
AnalyzerId = analyzerId ?? throw new ArgumentNullException(nameof(analyzerId));
ComponentKey = componentKey ?? throw new ArgumentNullException(nameof(componentKey));
@@ -23,6 +34,7 @@ public sealed class LanguageComponentRecord
Version = string.IsNullOrWhiteSpace(version) ? null : version.Trim();
Type = string.IsNullOrWhiteSpace(type) ? throw new ArgumentException("Type is required", nameof(type)) : type.Trim();
UsedByEntrypoint = usedByEntrypoint;
Intent = string.IsNullOrWhiteSpace(intent) ? null : intent.Trim();
_metadata = new SortedDictionary<string, string?>(StringComparer.Ordinal);
foreach (var entry in metadata ?? Array.Empty<KeyValuePair<string, string?>>())
@@ -45,6 +57,26 @@ public sealed class LanguageComponentRecord
_evidence[evidenceItem.ComparisonKey] = evidenceItem;
}
_capabilities = new List<string>();
foreach (var cap in capabilities ?? Array.Empty<string>())
{
if (!string.IsNullOrWhiteSpace(cap))
{
_capabilities.Add(cap.Trim());
}
}
_capabilities.Sort(StringComparer.Ordinal);
_threatVectors = new List<ComponentThreatVector>();
foreach (var threat in threatVectors ?? Array.Empty<ComponentThreatVector>())
{
if (threat is not null)
{
_threatVectors.Add(threat);
}
}
_threatVectors.Sort((a, b) => StringComparer.Ordinal.Compare(a.VectorType, b.VectorType));
}
public string AnalyzerId { get; }
@@ -61,6 +93,24 @@ public sealed class LanguageComponentRecord
public bool UsedByEntrypoint { get; private set; }
/// <summary>
/// Inferred application intent (e.g., "WebServer", "CliTool", "Worker").
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public string? Intent { get; private set; }
/// <summary>
/// Inferred capabilities (e.g., "NetworkListen", "FileWrite", "DatabaseAccess").
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public IReadOnlyList<string> Capabilities => _capabilities;
/// <summary>
/// Identified threat vectors with confidence scores.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public IReadOnlyList<ComponentThreatVector> ThreatVectors => _threatVectors;
public IReadOnlyDictionary<string, string?> Metadata => _metadata;
public IReadOnlyCollection<LanguageComponentEvidence> Evidence => _evidence.Values;
@@ -73,7 +123,10 @@ public sealed class LanguageComponentRecord
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
bool usedByEntrypoint = false,
string? intent = null,
IEnumerable<string>? capabilities = null,
IEnumerable<ComponentThreatVector>? threatVectors = null)
{
if (string.IsNullOrWhiteSpace(purl))
{
@@ -90,7 +143,10 @@ public sealed class LanguageComponentRecord
type,
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
evidence ?? Array.Empty<LanguageComponentEvidence>(),
usedByEntrypoint);
usedByEntrypoint,
intent,
capabilities,
threatVectors);
}
public static LanguageComponentRecord FromExplicitKey(
@@ -102,7 +158,10 @@ public sealed class LanguageComponentRecord
string type,
IEnumerable<KeyValuePair<string, string?>>? metadata = null,
IEnumerable<LanguageComponentEvidence>? evidence = null,
bool usedByEntrypoint = false)
bool usedByEntrypoint = false,
string? intent = null,
IEnumerable<string>? capabilities = null,
IEnumerable<ComponentThreatVector>? threatVectors = null)
{
if (string.IsNullOrWhiteSpace(componentKey))
{
@@ -118,7 +177,10 @@ public sealed class LanguageComponentRecord
type,
metadata ?? Array.Empty<KeyValuePair<string, string?>>(),
evidence ?? Array.Empty<LanguageComponentEvidence>(),
usedByEntrypoint);
usedByEntrypoint,
intent,
capabilities,
threatVectors);
}
internal static LanguageComponentRecord FromSnapshot(LanguageComponentSnapshot snapshot)
@@ -144,6 +206,17 @@ public sealed class LanguageComponentRecord
item.Sha256))
.ToArray();
var threatVectors = snapshot.ThreatVectors is null or { Count: 0 }
? Array.Empty<ComponentThreatVector>()
: snapshot.ThreatVectors
.Where(static item => item is not null)
.Select(static item => new ComponentThreatVector(
item.VectorType ?? string.Empty,
item.Confidence,
item.Evidence,
item.EntryPath))
.ToArray();
if (!string.IsNullOrWhiteSpace(snapshot.Purl))
{
return FromPurl(
@@ -154,7 +227,10 @@ public sealed class LanguageComponentRecord
snapshot.Type,
metadata,
evidence,
snapshot.UsedByEntrypoint);
snapshot.UsedByEntrypoint,
snapshot.Intent,
snapshot.Capabilities,
threatVectors);
}
return FromExplicitKey(
@@ -166,7 +242,10 @@ public sealed class LanguageComponentRecord
snapshot.Type,
metadata,
evidence,
snapshot.UsedByEntrypoint);
snapshot.UsedByEntrypoint,
snapshot.Intent,
snapshot.Capabilities,
threatVectors);
}
internal void Merge(LanguageComponentRecord other)
@@ -180,6 +259,34 @@ public sealed class LanguageComponentRecord
UsedByEntrypoint |= other.UsedByEntrypoint;
// Merge intent - prefer non-null
if (string.IsNullOrEmpty(Intent) && !string.IsNullOrEmpty(other.Intent))
{
Intent = other.Intent;
}
// Merge capabilities - union
foreach (var cap in other._capabilities)
{
if (!_capabilities.Contains(cap, StringComparer.Ordinal))
{
_capabilities.Add(cap);
}
}
_capabilities.Sort(StringComparer.Ordinal);
// Merge threat vectors - union by type
var existingTypes = new HashSet<string>(_threatVectors.Select(t => t.VectorType), StringComparer.Ordinal);
foreach (var threat in other._threatVectors)
{
if (!existingTypes.Contains(threat.VectorType))
{
_threatVectors.Add(threat);
existingTypes.Add(threat.VectorType);
}
}
_threatVectors.Sort((a, b) => StringComparer.Ordinal.Compare(a.VectorType, b.VectorType));
foreach (var entry in other._metadata)
{
if (!_metadata.TryGetValue(entry.Key, out var existing) || string.IsNullOrEmpty(existing))
@@ -194,6 +301,44 @@ public sealed class LanguageComponentRecord
}
}
/// <summary>
/// Sets semantic analysis data on this component.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public void SetSemantics(string? intent, IEnumerable<string>? capabilities, IEnumerable<ComponentThreatVector>? threatVectors)
{
if (!string.IsNullOrWhiteSpace(intent))
{
Intent = intent.Trim();
}
if (capabilities is not null)
{
foreach (var cap in capabilities)
{
if (!string.IsNullOrWhiteSpace(cap) && !_capabilities.Contains(cap.Trim(), StringComparer.Ordinal))
{
_capabilities.Add(cap.Trim());
}
}
_capabilities.Sort(StringComparer.Ordinal);
}
if (threatVectors is not null)
{
var existingTypes = new HashSet<string>(_threatVectors.Select(t => t.VectorType), StringComparer.Ordinal);
foreach (var threat in threatVectors)
{
if (threat is not null && !existingTypes.Contains(threat.VectorType))
{
_threatVectors.Add(threat);
existingTypes.Add(threat.VectorType);
}
}
_threatVectors.Sort((a, b) => StringComparer.Ordinal.Compare(a.VectorType, b.VectorType));
}
}
public LanguageComponentSnapshot ToSnapshot()
{
return new LanguageComponentSnapshot
@@ -205,6 +350,15 @@ public sealed class LanguageComponentRecord
Version = Version,
Type = Type,
UsedByEntrypoint = UsedByEntrypoint,
Intent = Intent,
Capabilities = _capabilities.ToArray(),
ThreatVectors = _threatVectors.Select(static item => new ComponentThreatVectorSnapshot
{
VectorType = item.VectorType,
Confidence = item.Confidence,
Evidence = item.Evidence,
EntryPath = item.EntryPath,
}).ToArray(),
Metadata = _metadata.ToDictionary(static pair => pair.Key, static pair => pair.Value, StringComparer.Ordinal),
Evidence = _evidence.Values.Select(static item => new LanguageComponentEvidenceSnapshot
{
@@ -218,6 +372,16 @@ public sealed class LanguageComponentRecord
}
}
/// <summary>
/// Represents an identified threat vector for a component.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public sealed record ComponentThreatVector(
string VectorType,
double Confidence,
string? Evidence,
string? EntryPath);
public sealed class LanguageComponentSnapshot
{
[JsonPropertyName("analyzerId")]
@@ -241,6 +405,27 @@ public sealed class LanguageComponentSnapshot
[JsonPropertyName("usedByEntrypoint")]
public bool UsedByEntrypoint { get; set; }
/// <summary>
/// Inferred application intent.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
[JsonPropertyName("intent")]
public string? Intent { get; set; }
/// <summary>
/// Inferred capabilities.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
[JsonPropertyName("capabilities")]
public IReadOnlyList<string> Capabilities { get; set; } = Array.Empty<string>();
/// <summary>
/// Identified threat vectors.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
[JsonPropertyName("threatVectors")]
public IReadOnlyList<ComponentThreatVectorSnapshot> ThreatVectors { get; set; } = Array.Empty<ComponentThreatVectorSnapshot>();
[JsonPropertyName("metadata")]
public IDictionary<string, string?> Metadata { get; set; } = new Dictionary<string, string?>(StringComparer.Ordinal);
@@ -248,6 +433,25 @@ public sealed class LanguageComponentSnapshot
public IReadOnlyList<LanguageComponentEvidenceSnapshot> Evidence { get; set; } = Array.Empty<LanguageComponentEvidenceSnapshot>();
}
/// <summary>
/// Snapshot representation of a threat vector.
/// </summary>
/// <remarks>Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).</remarks>
public sealed class ComponentThreatVectorSnapshot
{
[JsonPropertyName("vectorType")]
public string VectorType { get; set; } = string.Empty;
[JsonPropertyName("confidence")]
public double Confidence { get; set; }
[JsonPropertyName("evidence")]
public string? Evidence { get; set; }
[JsonPropertyName("entryPath")]
public string? EntryPath { get; set; }
}
public sealed class LanguageComponentEvidenceSnapshot
{
[JsonPropertyName("kind")]

View File

@@ -0,0 +1,261 @@
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Scanner.Analyzers.Lang;
/// <summary>
/// Semantic metadata field names for LanguageComponentRecord.
/// </summary>
/// <remarks>
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 18).
/// </remarks>
public static class SemanticMetadataFields
{
/// <summary>Application intent (WebServer, Worker, CliTool, etc.).</summary>
public const string Intent = "semantic:intent";
/// <summary>Comma-separated capability flags.</summary>
public const string Capabilities = "semantic:capabilities";
/// <summary>JSON array of threat vectors.</summary>
public const string ThreatVectors = "semantic:threatVectors";
/// <summary>Confidence score (0.0-1.0).</summary>
public const string Confidence = "semantic:confidence";
/// <summary>Confidence tier (Unknown, Low, Medium, High, Definitive).</summary>
public const string ConfidenceTier = "semantic:confidenceTier";
/// <summary>Framework name.</summary>
public const string Framework = "semantic:framework";
/// <summary>Framework version.</summary>
public const string FrameworkVersion = "semantic:frameworkVersion";
/// <summary>Whether this component is security-relevant.</summary>
public const string SecurityRelevant = "semantic:securityRelevant";
/// <summary>Risk score (0.0-1.0).</summary>
public const string RiskScore = "semantic:riskScore";
}
/// <summary>
/// Extension methods for accessing semantic data on LanguageComponentRecord.
/// </summary>
public static class LanguageComponentSemanticExtensions
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
/// <summary>Gets the inferred application intent.</summary>
public static string? GetIntent(this LanguageComponentRecord record)
{
return record.Metadata.TryGetValue(SemanticMetadataFields.Intent, out var value) ? value : null;
}
/// <summary>Gets the inferred capabilities as a list.</summary>
public static IReadOnlyList<string> GetCapabilities(this LanguageComponentRecord record)
{
if (!record.Metadata.TryGetValue(SemanticMetadataFields.Capabilities, out var value) ||
string.IsNullOrWhiteSpace(value))
{
return Array.Empty<string>();
}
return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
/// <summary>Gets the threat vectors as deserialized objects.</summary>
public static IReadOnlyList<ThreatVectorInfo> GetThreatVectors(this LanguageComponentRecord record)
{
if (!record.Metadata.TryGetValue(SemanticMetadataFields.ThreatVectors, out var value) ||
string.IsNullOrWhiteSpace(value))
{
return Array.Empty<ThreatVectorInfo>();
}
try
{
return JsonSerializer.Deserialize<ThreatVectorInfo[]>(value, JsonOptions)
?? Array.Empty<ThreatVectorInfo>();
}
catch
{
return Array.Empty<ThreatVectorInfo>();
}
}
/// <summary>Gets the confidence score.</summary>
public static double? GetConfidenceScore(this LanguageComponentRecord record)
{
if (!record.Metadata.TryGetValue(SemanticMetadataFields.Confidence, out var value) ||
string.IsNullOrWhiteSpace(value))
{
return null;
}
return double.TryParse(value, out var score) ? score : null;
}
/// <summary>Gets the confidence tier.</summary>
public static string? GetConfidenceTier(this LanguageComponentRecord record)
{
return record.Metadata.TryGetValue(SemanticMetadataFields.ConfidenceTier, out var value) ? value : null;
}
/// <summary>Gets the framework name.</summary>
public static string? GetFramework(this LanguageComponentRecord record)
{
return record.Metadata.TryGetValue(SemanticMetadataFields.Framework, out var value) ? value : null;
}
/// <summary>Gets whether this component is security-relevant.</summary>
public static bool IsSecurityRelevant(this LanguageComponentRecord record)
{
if (!record.Metadata.TryGetValue(SemanticMetadataFields.SecurityRelevant, out var value) ||
string.IsNullOrWhiteSpace(value))
{
return false;
}
return bool.TryParse(value, out var result) && result;
}
/// <summary>Gets the risk score.</summary>
public static double? GetRiskScore(this LanguageComponentRecord record)
{
if (!record.Metadata.TryGetValue(SemanticMetadataFields.RiskScore, out var value) ||
string.IsNullOrWhiteSpace(value))
{
return null;
}
return double.TryParse(value, out var score) ? score : null;
}
/// <summary>Checks if semantic data is present.</summary>
public static bool HasSemanticData(this LanguageComponentRecord record)
{
return record.Metadata.ContainsKey(SemanticMetadataFields.Intent) ||
record.Metadata.ContainsKey(SemanticMetadataFields.Capabilities);
}
}
/// <summary>
/// Builder for adding semantic metadata to LanguageComponentRecord.
/// </summary>
public sealed class SemanticMetadataBuilder
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly Dictionary<string, string?> _metadata = new(StringComparer.Ordinal);
public SemanticMetadataBuilder WithIntent(string intent)
{
_metadata[SemanticMetadataFields.Intent] = intent;
return this;
}
public SemanticMetadataBuilder WithCapabilities(IEnumerable<string> capabilities)
{
_metadata[SemanticMetadataFields.Capabilities] = string.Join(",", capabilities);
return this;
}
public SemanticMetadataBuilder WithCapabilities(long capabilityFlags, Func<long, IEnumerable<string>> flagsToNames)
{
_metadata[SemanticMetadataFields.Capabilities] = string.Join(",", flagsToNames(capabilityFlags));
return this;
}
public SemanticMetadataBuilder WithThreatVectors(IEnumerable<ThreatVectorInfo> threats)
{
var list = threats.ToList();
if (list.Count > 0)
{
_metadata[SemanticMetadataFields.ThreatVectors] = JsonSerializer.Serialize(list, JsonOptions);
}
return this;
}
public SemanticMetadataBuilder WithConfidence(double score, string tier)
{
_metadata[SemanticMetadataFields.Confidence] = score.ToString("F3");
_metadata[SemanticMetadataFields.ConfidenceTier] = tier;
return this;
}
public SemanticMetadataBuilder WithFramework(string framework, string? version = null)
{
_metadata[SemanticMetadataFields.Framework] = framework;
if (version is not null)
{
_metadata[SemanticMetadataFields.FrameworkVersion] = version;
}
return this;
}
public SemanticMetadataBuilder WithSecurityRelevant(bool relevant)
{
_metadata[SemanticMetadataFields.SecurityRelevant] = relevant.ToString().ToLowerInvariant();
return this;
}
public SemanticMetadataBuilder WithRiskScore(double score)
{
_metadata[SemanticMetadataFields.RiskScore] = score.ToString("F3");
return this;
}
public IEnumerable<KeyValuePair<string, string?>> Build()
{
return _metadata;
}
/// <summary>Merges semantic metadata with existing component metadata.</summary>
public IEnumerable<KeyValuePair<string, string?>> MergeWith(IEnumerable<KeyValuePair<string, string?>> existing)
{
var merged = new Dictionary<string, string?>(StringComparer.Ordinal);
foreach (var pair in existing)
{
merged[pair.Key] = pair.Value;
}
foreach (var pair in _metadata)
{
merged[pair.Key] = pair.Value;
}
return merged;
}
}
/// <summary>
/// Serializable threat vector information.
/// </summary>
public sealed class ThreatVectorInfo
{
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("confidence")]
public double Confidence { get; set; }
[JsonPropertyName("cweid")]
public int? CweId { get; set; }
[JsonPropertyName("owasp")]
public string? OwaspCategory { get; set; }
[JsonPropertyName("evidence")]
public IReadOnlyList<string>? Evidence { get; set; }
}