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
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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user