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

@@ -153,7 +153,7 @@ public sealed class CycloneDxComposer
}).OrderBy(entry => entry.LayerDigest, StringComparer.Ordinal).ToArray(),
};
var json = JsonSerializer.Serialize(recipe, new JsonSerializerOptions
var json = System.Text.Json.JsonSerializer.Serialize(recipe, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,

View File

@@ -0,0 +1,383 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using StellaOps.Scanner.EntryTrace.Semantic;
namespace StellaOps.Scanner.Emit.Composition;
/// <summary>
/// Property names for semantic entrypoint data in CycloneDX SBOMs.
/// </summary>
/// <remarks>
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 20).
/// Follows the stellaops:semantic.* namespace convention for SBOM properties.
/// </remarks>
public static class SemanticSbomPropertyNames
{
/// <summary>Application intent (WebServer, Worker, CliTool, etc.).</summary>
public const string Intent = "stellaops:semantic.intent";
/// <summary>Comma-separated capability flags.</summary>
public const string Capabilities = "stellaops:semantic.capabilities";
/// <summary>Number of detected capabilities.</summary>
public const string CapabilityCount = "stellaops:semantic.capability.count";
/// <summary>JSON array of threat vectors.</summary>
public const string ThreatVectors = "stellaops:semantic.threats";
/// <summary>Number of detected threat vectors.</summary>
public const string ThreatCount = "stellaops:semantic.threat.count";
/// <summary>Overall risk score (0.0-1.0).</summary>
public const string RiskScore = "stellaops:semantic.risk.score";
/// <summary>Confidence score (0.0-1.0).</summary>
public const string Confidence = "stellaops:semantic.confidence";
/// <summary>Confidence tier (Unknown, Low, Medium, High, Definitive).</summary>
public const string ConfidenceTier = "stellaops:semantic.confidence.tier";
/// <summary>Primary language.</summary>
public const string Language = "stellaops:semantic.language";
/// <summary>Framework name.</summary>
public const string Framework = "stellaops:semantic.framework";
/// <summary>Framework version.</summary>
public const string FrameworkVersion = "stellaops:semantic.framework.version";
/// <summary>Runtime version.</summary>
public const string RuntimeVersion = "stellaops:semantic.runtime.version";
/// <summary>Number of data flow boundaries.</summary>
public const string BoundaryCount = "stellaops:semantic.boundary.count";
/// <summary>Number of security-sensitive boundaries.</summary>
public const string SecuritySensitiveBoundaryCount = "stellaops:semantic.boundary.sensitive.count";
/// <summary>Comma-separated list of boundary types.</summary>
public const string BoundaryTypes = "stellaops:semantic.boundary.types";
/// <summary>Analysis timestamp (ISO-8601).</summary>
public const string AnalyzedAt = "stellaops:semantic.analyzed.at";
/// <summary>Semantic entrypoint ID.</summary>
public const string EntrypointId = "stellaops:semantic.entrypoint.id";
/// <summary>OWASP categories (comma-separated).</summary>
public const string OwaspCategories = "stellaops:semantic.owasp.categories";
/// <summary>CWE IDs (comma-separated).</summary>
public const string CweIds = "stellaops:semantic.cwe.ids";
}
/// <summary>
/// Extension methods for adding semantic entrypoint data to SBOM composition.
/// </summary>
public static class SemanticSbomExtensions
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
/// <summary>
/// Adds semantic entrypoint properties to the composition request.
/// </summary>
public static SbomCompositionRequest WithSemanticEntrypoint(
this SbomCompositionRequest request,
SemanticEntrypoint? entrypoint)
{
if (entrypoint is null)
return request;
var properties = BuildSemanticProperties(entrypoint);
var merged = MergeProperties(request.AdditionalProperties, properties);
return request with { AdditionalProperties = merged };
}
/// <summary>
/// Builds semantic properties from a semantic entrypoint.
/// </summary>
public static IReadOnlyDictionary<string, string> BuildSemanticProperties(SemanticEntrypoint entrypoint)
{
ArgumentNullException.ThrowIfNull(entrypoint);
var properties = new Dictionary<string, string>(StringComparer.Ordinal);
// Intent
properties[SemanticSbomPropertyNames.Intent] = entrypoint.Intent.ToString();
// Capabilities
var capabilityNames = GetCapabilityNames(entrypoint.Capabilities);
if (capabilityNames.Count > 0)
{
properties[SemanticSbomPropertyNames.Capabilities] = string.Join(",", capabilityNames);
properties[SemanticSbomPropertyNames.CapabilityCount] = capabilityNames.Count.ToString(CultureInfo.InvariantCulture);
}
// Attack surface / threat vectors
if (!entrypoint.AttackSurface.IsDefaultOrEmpty && entrypoint.AttackSurface.Length > 0)
{
var threatSummaries = entrypoint.AttackSurface
.Select(t => new
{
type = t.Type.ToString(),
confidence = t.Confidence,
cwe = t.Type.GetCweId()
})
.ToArray();
properties[SemanticSbomPropertyNames.ThreatVectors] = JsonSerializer.Serialize(threatSummaries, JsonOptions);
properties[SemanticSbomPropertyNames.ThreatCount] = entrypoint.AttackSurface.Length.ToString(CultureInfo.InvariantCulture);
// OWASP categories (via extension method on ThreatVectorType)
var owaspCategories = entrypoint.AttackSurface
.Select(t => t.Type.GetOwaspCategory())
.Where(owasp => !string.IsNullOrEmpty(owasp))
.Distinct()
.OrderBy(c => c, StringComparer.Ordinal)
.ToArray();
if (owaspCategories.Length > 0)
{
properties[SemanticSbomPropertyNames.OwaspCategories] = string.Join(",", owaspCategories);
}
// CWE IDs (via extension method on ThreatVectorType)
var cweIds = entrypoint.AttackSurface
.Select(t => t.Type.GetCweId())
.Where(cwe => cwe.HasValue)
.Select(cwe => cwe!.Value)
.Distinct()
.OrderBy(id => id)
.ToArray();
if (cweIds.Length > 0)
{
properties[SemanticSbomPropertyNames.CweIds] = string.Join(",", cweIds);
}
// Risk score (use max confidence as proxy for risk)
var maxRisk = entrypoint.AttackSurface.Max(t => t.Confidence);
properties[SemanticSbomPropertyNames.RiskScore] = FormatDouble(maxRisk);
}
// Data boundaries
if (!entrypoint.DataBoundaries.IsDefaultOrEmpty && entrypoint.DataBoundaries.Length > 0)
{
properties[SemanticSbomPropertyNames.BoundaryCount] =
entrypoint.DataBoundaries.Length.ToString(CultureInfo.InvariantCulture);
var sensitiveCount = entrypoint.DataBoundaries.Count(b => b.Type.IsSecuritySensitive());
if (sensitiveCount > 0)
{
properties[SemanticSbomPropertyNames.SecuritySensitiveBoundaryCount] =
sensitiveCount.ToString(CultureInfo.InvariantCulture);
}
var boundaryTypes = entrypoint.DataBoundaries
.Select(b => b.Type.ToString())
.Distinct()
.OrderBy(t => t, StringComparer.Ordinal)
.ToArray();
properties[SemanticSbomPropertyNames.BoundaryTypes] = string.Join(",", boundaryTypes);
}
// Confidence
properties[SemanticSbomPropertyNames.Confidence] = FormatDouble(entrypoint.Confidence.Score);
properties[SemanticSbomPropertyNames.ConfidenceTier] = entrypoint.Confidence.Tier.ToString();
// Language and framework
if (!string.IsNullOrEmpty(entrypoint.Language))
properties[SemanticSbomPropertyNames.Language] = entrypoint.Language;
if (!string.IsNullOrEmpty(entrypoint.Framework))
properties[SemanticSbomPropertyNames.Framework] = entrypoint.Framework;
if (!string.IsNullOrEmpty(entrypoint.FrameworkVersion))
properties[SemanticSbomPropertyNames.FrameworkVersion] = entrypoint.FrameworkVersion;
if (!string.IsNullOrEmpty(entrypoint.RuntimeVersion))
properties[SemanticSbomPropertyNames.RuntimeVersion] = entrypoint.RuntimeVersion;
// Entrypoint ID and timestamp
if (!string.IsNullOrEmpty(entrypoint.Id))
properties[SemanticSbomPropertyNames.EntrypointId] = entrypoint.Id;
if (!string.IsNullOrEmpty(entrypoint.AnalyzedAt))
properties[SemanticSbomPropertyNames.AnalyzedAt] = entrypoint.AnalyzedAt;
return properties;
}
/// <summary>
/// Extracts semantic entrypoint summary from SBOM properties.
/// </summary>
public static SemanticSbomSummary? ExtractSemanticSummary(IReadOnlyDictionary<string, string>? properties)
{
if (properties is null || properties.Count == 0)
return null;
if (!properties.TryGetValue(SemanticSbomPropertyNames.Intent, out var intentStr))
return null;
if (!Enum.TryParse<ApplicationIntent>(intentStr, ignoreCase: true, out var intent))
intent = ApplicationIntent.Unknown;
var summary = new SemanticSbomSummary
{
Intent = intent
};
if (properties.TryGetValue(SemanticSbomPropertyNames.Capabilities, out var caps) &&
!string.IsNullOrEmpty(caps))
{
summary = summary with
{
Capabilities = caps.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
}
if (properties.TryGetValue(SemanticSbomPropertyNames.ThreatCount, out var threatCountStr) &&
int.TryParse(threatCountStr, out var threatCount))
{
summary = summary with { ThreatCount = threatCount };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.RiskScore, out var riskStr) &&
double.TryParse(riskStr, out var risk))
{
summary = summary with { RiskScore = risk };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.Confidence, out var confStr) &&
double.TryParse(confStr, out var conf))
{
summary = summary with { Confidence = conf };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.ConfidenceTier, out var tier))
{
summary = summary with { ConfidenceTier = tier };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.Language, out var lang))
{
summary = summary with { Language = lang };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.Framework, out var fw))
{
summary = summary with { Framework = fw };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.BoundaryCount, out var boundaryCountStr) &&
int.TryParse(boundaryCountStr, out var boundaryCount))
{
summary = summary with { BoundaryCount = boundaryCount };
}
if (properties.TryGetValue(SemanticSbomPropertyNames.OwaspCategories, out var owasp) &&
!string.IsNullOrEmpty(owasp))
{
summary = summary with
{
OwaspCategories = owasp.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
};
}
if (properties.TryGetValue(SemanticSbomPropertyNames.CweIds, out var cweStr) &&
!string.IsNullOrEmpty(cweStr))
{
var cweIds = cweStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var id) ? id : (int?)null)
.Where(id => id.HasValue)
.Select(id => id!.Value)
.ToArray();
summary = summary with { CweIds = cweIds };
}
return summary;
}
/// <summary>
/// Checks if SBOM properties contain semantic data.
/// </summary>
public static bool HasSemanticData(IReadOnlyDictionary<string, string>? properties)
{
return properties?.ContainsKey(SemanticSbomPropertyNames.Intent) == true;
}
private static IReadOnlyList<string> GetCapabilityNames(CapabilityClass capabilities)
{
var names = new List<string>();
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
{
if (flag != CapabilityClass.None && !IsCompositeFlag(flag) && capabilities.HasFlag(flag))
{
names.Add(flag.ToString());
}
}
names.Sort(StringComparer.Ordinal);
return names;
}
private static bool IsCompositeFlag(CapabilityClass flag)
{
var val = (long)flag;
return val != 0 && (val & (val - 1)) != 0;
}
private static IReadOnlyDictionary<string, string> MergeProperties(
IReadOnlyDictionary<string, string>? existing,
IReadOnlyDictionary<string, string> newProperties)
{
var merged = new Dictionary<string, string>(StringComparer.Ordinal);
if (existing is not null)
{
foreach (var pair in existing)
{
merged[pair.Key] = pair.Value;
}
}
foreach (var pair in newProperties)
{
merged[pair.Key] = pair.Value;
}
return merged;
}
private static string FormatDouble(double value)
=> value.ToString("0.####", CultureInfo.InvariantCulture);
}
/// <summary>
/// Summary of semantic entrypoint data extracted from SBOM properties.
/// </summary>
public sealed record SemanticSbomSummary
{
public ApplicationIntent Intent { get; init; } = ApplicationIntent.Unknown;
public IReadOnlyList<string> Capabilities { get; init; } = Array.Empty<string>();
public int ThreatCount { get; init; }
public double RiskScore { get; init; }
public double Confidence { get; init; }
public string? ConfidenceTier { get; init; }
public string? Language { get; init; }
public string? Framework { get; init; }
public int BoundaryCount { get; init; }
public IReadOnlyList<string> OwaspCategories { get; init; } = Array.Empty<string>();
public IReadOnlyList<int> CweIds { get; init; } = Array.Empty<int>();
/// <summary>
/// Returns true if this summary indicates high security relevance.
/// </summary>
public bool IsSecurityRelevant => ThreatCount > 0 || RiskScore > 0.5 || CweIds.Count > 0;
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.EntryTrace\StellaOps.Scanner.EntryTrace.csproj" />
<ProjectReference Include="..\StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj" />
</ItemGroup>