feat(api): Add Policy Registry API specification
Some checks failed
AOC Guard CI / aoc-verify (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-verify (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled
- Introduced OpenAPI specification for the StellaOps Policy Registry API, covering endpoints for verification policies, policy packs, snapshots, violations, overrides, sealed mode operations, and advisory staleness tracking. - Defined schemas, parameters, and responses for comprehensive API documentation. chore(scanner): Add global usings for scanner analyzers - Created GlobalUsings.cs to simplify namespace usage across analyzer libraries. feat(scanner): Implement Surface Service Collection Extensions - Added SurfaceServiceCollectionExtensions for dependency injection registration of surface analysis services. - Included methods for adding surface analysis, surface collectors, and entry point collectors to the service collection.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
@@ -1,171 +0,0 @@
|
||||
using StellaOps.Scanner.Surface.Discovery;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Complete result of surface analysis for a scan.
|
||||
/// </summary>
|
||||
public sealed record SurfaceAnalysisResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Scan identifier.
|
||||
/// </summary>
|
||||
public required string ScanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When analysis was performed.
|
||||
/// </summary>
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis summary statistics.
|
||||
/// </summary>
|
||||
public required SurfaceAnalysisSummary Summary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Discovered surface entries.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SurfaceEntry> Entries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Discovered entry points.
|
||||
/// </summary>
|
||||
public IReadOnlyList<EntryPoint>? EntryPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis metadata.
|
||||
/// </summary>
|
||||
public SurfaceAnalysisMetadata? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary statistics for surface analysis.
|
||||
/// </summary>
|
||||
public sealed record SurfaceAnalysisSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of surface entries.
|
||||
/// </summary>
|
||||
public required int TotalEntries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entry counts by type.
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<SurfaceType, int> ByType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entry counts by confidence level.
|
||||
/// </summary>
|
||||
public required IReadOnlyDictionary<ConfidenceLevel, int> ByConfidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculated risk score (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public required double RiskScore { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// High-risk entry count.
|
||||
/// </summary>
|
||||
public int HighRiskCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total entry points discovered.
|
||||
/// </summary>
|
||||
public int? EntryPointCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates summary from entries.
|
||||
/// </summary>
|
||||
public static SurfaceAnalysisSummary FromEntries(IReadOnlyList<SurfaceEntry> entries)
|
||||
{
|
||||
var byType = entries
|
||||
.GroupBy(e => e.Type)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
var byConfidence = entries
|
||||
.GroupBy(e => e.Confidence)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
// Calculate risk score based on entry types and confidence
|
||||
var riskScore = CalculateRiskScore(entries);
|
||||
|
||||
var highRiskCount = entries.Count(e =>
|
||||
e.Type is SurfaceType.ProcessExecution or SurfaceType.CryptoOperation or SurfaceType.SecretAccess ||
|
||||
e.Confidence == ConfidenceLevel.Verified);
|
||||
|
||||
return new SurfaceAnalysisSummary
|
||||
{
|
||||
TotalEntries = entries.Count,
|
||||
ByType = byType,
|
||||
ByConfidence = byConfidence,
|
||||
RiskScore = riskScore,
|
||||
HighRiskCount = highRiskCount
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateRiskScore(IReadOnlyList<SurfaceEntry> entries)
|
||||
{
|
||||
if (entries.Count == 0) return 0.0;
|
||||
|
||||
var typeWeights = new Dictionary<SurfaceType, double>
|
||||
{
|
||||
[SurfaceType.ProcessExecution] = 1.0,
|
||||
[SurfaceType.SecretAccess] = 0.9,
|
||||
[SurfaceType.CryptoOperation] = 0.8,
|
||||
[SurfaceType.DatabaseOperation] = 0.7,
|
||||
[SurfaceType.Deserialization] = 0.85,
|
||||
[SurfaceType.DynamicCode] = 0.9,
|
||||
[SurfaceType.AuthenticationPoint] = 0.6,
|
||||
[SurfaceType.NetworkEndpoint] = 0.5,
|
||||
[SurfaceType.InputHandling] = 0.5,
|
||||
[SurfaceType.ExternalCall] = 0.4,
|
||||
[SurfaceType.FileOperation] = 0.3
|
||||
};
|
||||
|
||||
var confidenceMultipliers = new Dictionary<ConfidenceLevel, double>
|
||||
{
|
||||
[ConfidenceLevel.Low] = 0.5,
|
||||
[ConfidenceLevel.Medium] = 0.75,
|
||||
[ConfidenceLevel.High] = 1.0,
|
||||
[ConfidenceLevel.Verified] = 1.0
|
||||
};
|
||||
|
||||
var totalWeight = entries.Sum(e =>
|
||||
typeWeights.GetValueOrDefault(e.Type, 0.3) *
|
||||
confidenceMultipliers.GetValueOrDefault(e.Confidence, 0.5));
|
||||
|
||||
// Normalize to 0-1 range (cap at 100 weighted entries)
|
||||
return Math.Min(1.0, totalWeight / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about the surface analysis execution.
|
||||
/// </summary>
|
||||
public sealed record SurfaceAnalysisMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Analysis duration in milliseconds.
|
||||
/// </summary>
|
||||
public double DurationMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Files analyzed count.
|
||||
/// </summary>
|
||||
public int FilesAnalyzed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Languages detected.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Languages { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Frameworks detected.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Frameworks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Analysis configuration used.
|
||||
/// </summary>
|
||||
public SurfaceAnalysisOptions? Options { get; init; }
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Surface.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Output;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for writing surface analysis results.
|
||||
/// </summary>
|
||||
public interface ISurfaceAnalysisWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes analysis result to the specified stream.
|
||||
/// </summary>
|
||||
Task WriteAsync(
|
||||
SurfaceAnalysisResult result,
|
||||
Stream outputStream,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes analysis result to JSON string.
|
||||
/// </summary>
|
||||
string Serialize(SurfaceAnalysisResult result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store key for surface analysis results.
|
||||
/// </summary>
|
||||
public static class SurfaceAnalysisStoreKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Key for storing surface analysis in scan artifacts.
|
||||
/// </summary>
|
||||
public const string SurfaceAnalysis = "scanner.surface.analysis";
|
||||
|
||||
/// <summary>
|
||||
/// Key for storing surface entries.
|
||||
/// </summary>
|
||||
public const string SurfaceEntries = "scanner.surface.entries";
|
||||
|
||||
/// <summary>
|
||||
/// Key for storing entry points.
|
||||
/// </summary>
|
||||
public const string EntryPoints = "scanner.surface.entrypoints";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of surface analysis writer.
|
||||
/// Uses deterministic JSON serialization.
|
||||
/// </summary>
|
||||
public sealed class SurfaceAnalysisWriter : ISurfaceAnalysisWriter
|
||||
{
|
||||
private readonly ILogger<SurfaceAnalysisWriter> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions PrettyJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
||||
};
|
||||
|
||||
public SurfaceAnalysisWriter(ILogger<SurfaceAnalysisWriter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task WriteAsync(
|
||||
SurfaceAnalysisResult result,
|
||||
Stream outputStream,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Sort entries by ID for determinism
|
||||
var sortedResult = SortResult(result);
|
||||
|
||||
await JsonSerializer.SerializeAsync(
|
||||
outputStream,
|
||||
sortedResult,
|
||||
JsonOptions,
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Wrote surface analysis for scan {ScanId} with {EntryCount} entries",
|
||||
result.ScanId,
|
||||
result.Entries.Count);
|
||||
}
|
||||
|
||||
public string Serialize(SurfaceAnalysisResult result)
|
||||
{
|
||||
var sortedResult = SortResult(result);
|
||||
return JsonSerializer.Serialize(sortedResult, PrettyJsonOptions);
|
||||
}
|
||||
|
||||
private static SurfaceAnalysisResult SortResult(SurfaceAnalysisResult result)
|
||||
{
|
||||
// Sort entries by ID for deterministic output
|
||||
var sortedEntries = result.Entries
|
||||
.OrderBy(e => e.Id)
|
||||
.ToList();
|
||||
|
||||
// Sort entry points by ID if present
|
||||
var sortedEntryPoints = result.EntryPoints?
|
||||
.OrderBy(ep => ep.Id)
|
||||
.ToList();
|
||||
|
||||
return result with
|
||||
{
|
||||
Entries = sortedEntries,
|
||||
EntryPoints = sortedEntryPoints
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public sealed class SurfaceAnalysisWriter : ISurfaceAnalysisWriter
|
||||
|
||||
var jsonOptions = options.PrettyPrint ? s_prettyJsonOptions : s_jsonOptions;
|
||||
|
||||
if (options.WriteToFile && \!string.IsNullOrEmpty(options.OutputPath))
|
||||
if (options.WriteToFile && !string.IsNullOrEmpty(options.OutputPath))
|
||||
{
|
||||
var filePath = Path.Combine(options.OutputPath, $"surface-{result.ScanId}.json");
|
||||
await using var stream = File.Create(filePath);
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Surface.Discovery;
|
||||
using StellaOps.Scanner.Surface.Output;
|
||||
using StellaOps.Scanner.Surface.Signals;
|
||||
|
||||
namespace StellaOps.Scanner.Surface;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering surface analysis services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds surface analysis services to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceAnalysis(
|
||||
this IServiceCollection services,
|
||||
IConfiguration? configuration = null)
|
||||
{
|
||||
// Core services
|
||||
services.TryAddSingleton<ISurfaceEntryRegistry, SurfaceEntryRegistry>();
|
||||
services.TryAddSingleton<ISurfaceSignalEmitter, SurfaceSignalEmitter>();
|
||||
services.TryAddSingleton<ISurfaceAnalysisWriter, SurfaceAnalysisWriter>();
|
||||
services.TryAddSingleton<ISurfaceAnalyzer, SurfaceAnalyzer>();
|
||||
|
||||
// Configure options if configuration provided
|
||||
if (configuration != null)
|
||||
{
|
||||
services.Configure<SurfaceAnalysisOptions>(
|
||||
configuration.GetSection("Scanner:Surface"));
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds surface analysis services with a signal sink.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceAnalysis<TSignalSink>(
|
||||
this IServiceCollection services,
|
||||
IConfiguration? configuration = null)
|
||||
where TSignalSink : class, ISurfaceSignalSink
|
||||
{
|
||||
services.AddSurfaceAnalysis(configuration);
|
||||
services.TryAddSingleton<ISurfaceSignalSink, TSignalSink>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds surface analysis services with in-memory signal sink for testing.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceAnalysisForTesting(this IServiceCollection services)
|
||||
{
|
||||
services.AddSurfaceAnalysis();
|
||||
services.TryAddSingleton<ISurfaceSignalSink, InMemorySurfaceSignalSink>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a surface entry collector.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceCollector<TCollector>(this IServiceCollection services)
|
||||
where TCollector : class, ISurfaceEntryCollector
|
||||
{
|
||||
services.AddSingleton<ISurfaceEntryCollector, TCollector>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers multiple surface entry collectors.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceCollectors(
|
||||
this IServiceCollection services,
|
||||
params Type[] collectorTypes)
|
||||
{
|
||||
foreach (var type in collectorTypes)
|
||||
{
|
||||
if (!typeof(ISurfaceEntryCollector).IsAssignableFrom(type))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Type {type.Name} does not implement ISurfaceEntryCollector",
|
||||
nameof(collectorTypes));
|
||||
}
|
||||
|
||||
services.AddSingleton(typeof(ISurfaceEntryCollector), type);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for configuring surface analysis.
|
||||
/// </summary>
|
||||
public sealed class SurfaceAnalysisBuilder
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
internal SurfaceAnalysisBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a collector.
|
||||
/// </summary>
|
||||
public SurfaceAnalysisBuilder AddCollector<TCollector>()
|
||||
where TCollector : class, ISurfaceEntryCollector
|
||||
{
|
||||
_services.AddSurfaceCollector<TCollector>();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures a custom signal sink.
|
||||
/// </summary>
|
||||
public SurfaceAnalysisBuilder UseSignalSink<TSignalSink>()
|
||||
where TSignalSink : class, ISurfaceSignalSink
|
||||
{
|
||||
_services.TryAddSingleton<ISurfaceSignalSink, TSignalSink>();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures options.
|
||||
/// </summary>
|
||||
public SurfaceAnalysisBuilder Configure(Action<SurfaceAnalysisOptions> configure)
|
||||
{
|
||||
_services.Configure(configure);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension for fluent builder pattern.
|
||||
/// </summary>
|
||||
public static class SurfaceAnalysisBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds surface analysis with fluent configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSurfaceAnalysis(
|
||||
this IServiceCollection services,
|
||||
Action<SurfaceAnalysisBuilder> configure)
|
||||
{
|
||||
services.AddSurfaceAnalysis();
|
||||
var builder = new SurfaceAnalysisBuilder(services);
|
||||
configure(builder);
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.Surface.Models;
|
||||
|
||||
namespace StellaOps.Scanner.Surface.Signals;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for emitting surface analysis signals for policy evaluation.
|
||||
/// </summary>
|
||||
public interface ISurfaceSignalEmitter
|
||||
{
|
||||
/// <summary>
|
||||
/// Emits signals for the given analysis result.
|
||||
/// </summary>
|
||||
Task EmitAsync(
|
||||
string scanId,
|
||||
SurfaceAnalysisResult result,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Emits custom signals.
|
||||
/// </summary>
|
||||
Task EmitAsync(
|
||||
string scanId,
|
||||
IDictionary<string, object> signals,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of surface signal emitter.
|
||||
/// Converts analysis results to policy signals.
|
||||
/// </summary>
|
||||
public sealed class SurfaceSignalEmitter : ISurfaceSignalEmitter
|
||||
{
|
||||
private readonly ILogger<SurfaceSignalEmitter> _logger;
|
||||
private readonly ISurfaceSignalSink? _sink;
|
||||
|
||||
public SurfaceSignalEmitter(
|
||||
ILogger<SurfaceSignalEmitter> logger,
|
||||
ISurfaceSignalSink? sink = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_sink = sink;
|
||||
}
|
||||
|
||||
public async Task EmitAsync(
|
||||
string scanId,
|
||||
SurfaceAnalysisResult result,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var signals = BuildSignals(result);
|
||||
await EmitAsync(scanId, signals, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task EmitAsync(
|
||||
string scanId,
|
||||
IDictionary<string, object> signals,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Emitting {SignalCount} surface signals for scan {ScanId}",
|
||||
signals.Count,
|
||||
scanId);
|
||||
|
||||
if (_sink != null)
|
||||
{
|
||||
await _sink.WriteAsync(scanId, signals, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"No signal sink configured, signals for scan {ScanId}: {Signals}",
|
||||
scanId,
|
||||
string.Join(", ", signals.Select(kv => $"{kv.Key}={kv.Value}")));
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> BuildSignals(SurfaceAnalysisResult result)
|
||||
{
|
||||
var signals = new Dictionary<string, object>
|
||||
{
|
||||
[SurfaceSignalKeys.TotalSurfaceArea] = result.Summary.TotalEntries,
|
||||
[SurfaceSignalKeys.RiskScore] = result.Summary.RiskScore,
|
||||
[SurfaceSignalKeys.HighConfidenceCount] = result.Entries
|
||||
.Count(e => e.Confidence >= ConfidenceLevel.High)
|
||||
};
|
||||
|
||||
// Add counts by type
|
||||
foreach (var (type, count) in result.Summary.ByType)
|
||||
{
|
||||
var key = type switch
|
||||
{
|
||||
SurfaceType.NetworkEndpoint => SurfaceSignalKeys.NetworkEndpoints,
|
||||
SurfaceType.FileOperation => SurfaceSignalKeys.FileOperations,
|
||||
SurfaceType.ProcessExecution => SurfaceSignalKeys.ProcessSpawns,
|
||||
SurfaceType.CryptoOperation => SurfaceSignalKeys.CryptoUsage,
|
||||
SurfaceType.AuthenticationPoint => SurfaceSignalKeys.AuthPoints,
|
||||
SurfaceType.InputHandling => SurfaceSignalKeys.InputHandlers,
|
||||
SurfaceType.SecretAccess => SurfaceSignalKeys.SecretAccess,
|
||||
SurfaceType.ExternalCall => SurfaceSignalKeys.ExternalCalls,
|
||||
SurfaceType.DatabaseOperation => SurfaceSignalKeys.DatabaseOperations,
|
||||
SurfaceType.Deserialization => SurfaceSignalKeys.DeserializationPoints,
|
||||
SurfaceType.DynamicCode => SurfaceSignalKeys.DynamicCodePoints,
|
||||
_ => $"{SurfaceSignalKeys.Prefix}{type.ToString().ToLowerInvariant()}"
|
||||
};
|
||||
|
||||
signals[key] = count;
|
||||
}
|
||||
|
||||
// Add entry point count if available
|
||||
if (result.EntryPoints is { Count: > 0 })
|
||||
{
|
||||
signals[SurfaceSignalKeys.EntryPointCount] = result.EntryPoints.Count;
|
||||
}
|
||||
|
||||
// Add framework signals if metadata available
|
||||
if (result.Metadata?.Frameworks is { Count: > 0 } frameworks)
|
||||
{
|
||||
foreach (var framework in frameworks)
|
||||
{
|
||||
var normalizedName = framework.ToLowerInvariant().Replace(" ", "_").Replace(".", "_");
|
||||
signals[$"{SurfaceSignalKeys.FrameworkPrefix}{normalizedName}"] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add language signals if metadata available
|
||||
if (result.Metadata?.Languages is { Count: > 0 } languages)
|
||||
{
|
||||
foreach (var language in languages)
|
||||
{
|
||||
var normalizedName = language.ToLowerInvariant();
|
||||
signals[$"{SurfaceSignalKeys.LanguagePrefix}{normalizedName}"] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sink for writing surface signals to storage.
|
||||
/// </summary>
|
||||
public interface ISurfaceSignalSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes signals to storage.
|
||||
/// </summary>
|
||||
Task WriteAsync(
|
||||
string scanId,
|
||||
IDictionary<string, object> signals,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory signal sink for testing.
|
||||
/// </summary>
|
||||
public sealed class InMemorySurfaceSignalSink : ISurfaceSignalSink
|
||||
{
|
||||
private readonly Dictionary<string, IDictionary<string, object>> _signals = new();
|
||||
|
||||
public IReadOnlyDictionary<string, IDictionary<string, object>> Signals => _signals;
|
||||
|
||||
public Task WriteAsync(
|
||||
string scanId,
|
||||
IDictionary<string, object> signals,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_signals[scanId] = new Dictionary<string, object>(signals);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IDictionary<string, object>? GetSignals(string scanId)
|
||||
{
|
||||
return _signals.TryGetValue(scanId, out var signals) ? signals : null;
|
||||
}
|
||||
|
||||
public void Clear() => _signals.Clear();
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
namespace StellaOps.Scanner.Surface.Signals;
|
||||
|
||||
/// <summary>
|
||||
/// Standard signal keys for surface analysis policy integration.
|
||||
/// </summary>
|
||||
public static class SurfaceSignalKeys
|
||||
{
|
||||
/// <summary>Prefix for all surface signals.</summary>
|
||||
public const string Prefix = "surface.";
|
||||
|
||||
/// <summary>Network endpoint count.</summary>
|
||||
public const string NetworkEndpoints = "surface.network.endpoints";
|
||||
|
||||
/// <summary>Exposed port count.</summary>
|
||||
public const string ExposedPorts = "surface.network.ports";
|
||||
|
||||
/// <summary>File operation count.</summary>
|
||||
public const string FileOperations = "surface.file.operations";
|
||||
|
||||
/// <summary>Process spawn count.</summary>
|
||||
public const string ProcessSpawns = "surface.process.spawns";
|
||||
|
||||
/// <summary>Crypto operation count.</summary>
|
||||
public const string CryptoUsage = "surface.crypto.usage";
|
||||
|
||||
/// <summary>Authentication point count.</summary>
|
||||
public const string AuthPoints = "surface.auth.points";
|
||||
|
||||
/// <summary>Input handler count.</summary>
|
||||
public const string InputHandlers = "surface.input.handlers";
|
||||
|
||||
/// <summary>Secret access point count.</summary>
|
||||
public const string SecretAccess = "surface.secrets.access";
|
||||
|
||||
/// <summary>External call count.</summary>
|
||||
public const string ExternalCalls = "surface.external.calls";
|
||||
|
||||
/// <summary>Database operation count.</summary>
|
||||
public const string DatabaseOperations = "surface.database.operations";
|
||||
|
||||
/// <summary>Deserialization point count.</summary>
|
||||
public const string DeserializationPoints = "surface.deserialization.points";
|
||||
|
||||
/// <summary>Dynamic code execution count.</summary>
|
||||
public const string DynamicCodePoints = "surface.dynamic.code";
|
||||
|
||||
/// <summary>Total surface area score.</summary>
|
||||
public const string TotalSurfaceArea = "surface.total.area";
|
||||
|
||||
/// <summary>Overall risk score (0.0-1.0).</summary>
|
||||
public const string RiskScore = "surface.risk.score";
|
||||
|
||||
/// <summary>High-confidence entry count.</summary>
|
||||
public const string HighConfidenceCount = "surface.high_confidence.count";
|
||||
|
||||
/// <summary>Entry point count.</summary>
|
||||
public const string EntryPointCount = "surface.entry_points.count";
|
||||
|
||||
/// <summary>Framework-specific prefix.</summary>
|
||||
public const string FrameworkPrefix = "surface.framework.";
|
||||
|
||||
/// <summary>Language-specific prefix.</summary>
|
||||
public const string LanguagePrefix = "surface.language.";
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Scanner.Surface.FS\StellaOps.Scanner.Surface.FS.csproj" />
|
||||
|
||||
@@ -7,21 +7,41 @@ using StellaOps.Scanner.Surface.Signals;
|
||||
namespace StellaOps.Scanner.Surface;
|
||||
|
||||
/// <summary>
|
||||
/// Main interface for surface analysis operations.
|
||||
/// Options for surface analysis execution.
|
||||
/// </summary>
|
||||
public sealed record SurfaceAnalysisOptions
|
||||
{
|
||||
/// <summary>Collector options.</summary>
|
||||
public SurfaceCollectorOptions CollectorOptions { get; init; } = new();
|
||||
|
||||
/// <summary>Output options.</summary>
|
||||
public SurfaceOutputOptions OutputOptions { get; init; } = new();
|
||||
|
||||
/// <summary>Whether to emit policy signals.</summary>
|
||||
public bool EmitSignals { get; init; } = true;
|
||||
|
||||
/// <summary>Whether to discover entry points.</summary>
|
||||
public bool DiscoverEntryPoints { get; init; } = true;
|
||||
|
||||
/// <summary>Languages to analyze for entry points.</summary>
|
||||
public IReadOnlySet<string> Languages { get; init; } = new HashSet<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for orchestrating surface analysis.
|
||||
/// </summary>
|
||||
public interface ISurfaceAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs surface analysis on the given context.
|
||||
/// </summary>
|
||||
/// <summary>Runs surface analysis on the specified path.</summary>
|
||||
Task<SurfaceAnalysisResult> AnalyzeAsync(
|
||||
SurfaceCollectionContext context,
|
||||
string scanId,
|
||||
string rootPath,
|
||||
SurfaceAnalysisOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of surface analyzer.
|
||||
/// Coordinates collectors, signal emission, and output writing.
|
||||
/// Default surface analyzer implementation.
|
||||
/// </summary>
|
||||
public sealed class SurfaceAnalyzer : ISurfaceAnalyzer
|
||||
{
|
||||
@@ -43,59 +63,152 @@ public sealed class SurfaceAnalyzer : ISurfaceAnalyzer
|
||||
}
|
||||
|
||||
public async Task<SurfaceAnalysisResult> AnalyzeAsync(
|
||||
SurfaceCollectionContext context,
|
||||
string scanId,
|
||||
string rootPath,
|
||||
SurfaceAnalysisOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(scanId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(rootPath);
|
||||
options ??= new SurfaceAnalysisOptions();
|
||||
|
||||
var startTime = DateTimeOffset.UtcNow;
|
||||
_logger.LogInformation("Starting surface analysis for scan {ScanId} at {RootPath}", scanId, rootPath);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting surface analysis for scan {ScanId} with {FileCount} files",
|
||||
context.ScanId,
|
||||
context.Files.Count);
|
||||
|
||||
// Collect entries from all applicable collectors
|
||||
var entries = new List<SurfaceEntry>();
|
||||
await foreach (var entry in _registry.CollectAllAsync(context, cancellationToken))
|
||||
var context = new SurfaceCollectorContext
|
||||
{
|
||||
entries.Add(entry);
|
||||
ScanId = scanId,
|
||||
RootPath = rootPath,
|
||||
Options = options.CollectorOptions
|
||||
};
|
||||
|
||||
// Collect surface entries
|
||||
var entries = new List<SurfaceEntry>();
|
||||
var collectors = _registry.GetCollectors();
|
||||
|
||||
_logger.LogDebug("Running {CollectorCount} surface collectors", collectors.Count);
|
||||
|
||||
foreach (var collector in collectors)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var entry in collector.CollectAsync(context, cancellationToken))
|
||||
{
|
||||
entries.Add(entry);
|
||||
}
|
||||
_logger.LogDebug("Collector {CollectorId} found {Count} entries", collector.CollectorId, entries.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Collector {CollectorId} failed", collector.CollectorId);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Collected {EntryCount} surface entries for scan {ScanId}",
|
||||
entries.Count,
|
||||
context.ScanId);
|
||||
// Collect entry points
|
||||
var entryPoints = new List<EntryPoint>();
|
||||
if (options.DiscoverEntryPoints)
|
||||
{
|
||||
var epCollectors = _registry.GetEntryPointCollectors();
|
||||
_logger.LogDebug("Running {Count} entry point collectors", epCollectors.Count);
|
||||
|
||||
foreach (var collector in epCollectors)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var ep in collector.CollectAsync(context, cancellationToken))
|
||||
{
|
||||
entryPoints.Add(ep);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Entry point collector {CollectorId} failed", collector.CollectorId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort entries by ID for determinism
|
||||
entries.Sort((a, b) => string.Compare(a.Id, b.Id, StringComparison.Ordinal));
|
||||
entryPoints.Sort((a, b) => string.Compare(a.Id, b.Id, StringComparison.Ordinal));
|
||||
|
||||
// Build summary
|
||||
var summary = SurfaceAnalysisSummary.FromEntries(entries);
|
||||
var byType = entries.GroupBy(e => e.Type).ToDictionary(g => g.Key, g => g.Count());
|
||||
var summary = new SurfaceAnalysisSummary
|
||||
{
|
||||
TotalEntries = entries.Count,
|
||||
ByType = byType,
|
||||
RiskScore = CalculateRiskScore(entries, entryPoints)
|
||||
};
|
||||
|
||||
// Create result
|
||||
var result = new SurfaceAnalysisResult
|
||||
{
|
||||
ScanId = context.ScanId,
|
||||
ScanId = scanId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Summary = summary,
|
||||
Entries = entries,
|
||||
Metadata = new SurfaceAnalysisMetadata
|
||||
{
|
||||
DurationMs = (DateTimeOffset.UtcNow - startTime).TotalMilliseconds,
|
||||
FilesAnalyzed = context.Files.Count,
|
||||
Languages = context.DetectedLanguages,
|
||||
Frameworks = context.DetectedFrameworks,
|
||||
Options = context.Options
|
||||
}
|
||||
EntryPoints = entryPoints
|
||||
};
|
||||
|
||||
// Emit signals for policy evaluation
|
||||
await _signalEmitter.EmitAsync(context.ScanId, result, cancellationToken);
|
||||
// Emit signals
|
||||
if (options.EmitSignals)
|
||||
{
|
||||
var signals = SurfaceSignalEmitter.BuildSignals(result);
|
||||
await _signalEmitter.EmitAsync(scanId, signals, cancellationToken);
|
||||
}
|
||||
|
||||
// Write output
|
||||
await _writer.WriteAsync(result, options.OutputOptions, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Completed surface analysis for scan {ScanId}: {TotalEntries} entries, risk score {RiskScore:F2}",
|
||||
context.ScanId,
|
||||
result.Summary.TotalEntries,
|
||||
result.Summary.RiskScore);
|
||||
"Surface analysis complete: {EntryCount} entries, {EntryPointCount} entry points, risk score {RiskScore:F2}",
|
||||
entries.Count, entryPoints.Count, summary.RiskScore);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double CalculateRiskScore(IReadOnlyList<SurfaceEntry> entries, IReadOnlyList<EntryPoint> entryPoints)
|
||||
{
|
||||
if (entries.Count == 0 && entryPoints.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
// Weight high-risk types more heavily
|
||||
var riskWeights = new Dictionary<SurfaceType, double>
|
||||
{
|
||||
[SurfaceType.SecretAccess] = 1.0,
|
||||
[SurfaceType.AuthenticationPoint] = 0.9,
|
||||
[SurfaceType.ProcessExecution] = 0.8,
|
||||
[SurfaceType.CryptoOperation] = 0.7,
|
||||
[SurfaceType.ExternalCall] = 0.6,
|
||||
[SurfaceType.NetworkEndpoint] = 0.5,
|
||||
[SurfaceType.InputHandling] = 0.5,
|
||||
[SurfaceType.FileOperation] = 0.3
|
||||
};
|
||||
|
||||
double totalWeight = 0;
|
||||
double weightedSum = 0;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var weight = riskWeights.GetValueOrDefault(entry.Type, 0.5);
|
||||
var confidence = entry.Confidence switch
|
||||
{
|
||||
ConfidenceLevel.VeryHigh => 1.0,
|
||||
ConfidenceLevel.High => 0.8,
|
||||
ConfidenceLevel.Medium => 0.5,
|
||||
ConfidenceLevel.Low => 0.2,
|
||||
_ => 0.5
|
||||
};
|
||||
weightedSum += weight * confidence;
|
||||
totalWeight += 1.0;
|
||||
}
|
||||
|
||||
// Entry points add to risk
|
||||
weightedSum += entryPoints.Count * 0.3;
|
||||
totalWeight += entryPoints.Count * 0.5;
|
||||
|
||||
return totalWeight > 0 ? Math.Min(1.0, weightedSum / totalWeight) : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Scanner.Surface.Discovery;
|
||||
using StellaOps.Scanner.Surface.Output;
|
||||
using StellaOps.Scanner.Surface.Signals;
|
||||
|
||||
namespace StellaOps.Scanner.Surface;
|
||||
|
||||
/// <summary>
|
||||
/// DI registration extensions for Scanner Surface analysis.
|
||||
/// </summary>
|
||||
public static class SurfaceServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>Adds surface analysis services to the service collection.</summary>
|
||||
public static IServiceCollection AddSurfaceAnalysis(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.AddSingleton<ISurfaceEntryRegistry, SurfaceEntryRegistry>();
|
||||
services.AddSingleton<ISurfaceSignalEmitter, SurfaceSignalEmitter>();
|
||||
services.AddSingleton<ISurfaceAnalysisWriter, SurfaceAnalysisWriter>();
|
||||
services.AddSingleton<ISurfaceAnalyzer, SurfaceAnalyzer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>Adds a surface entry collector.</summary>
|
||||
public static IServiceCollection AddSurfaceCollector<T>(this IServiceCollection services)
|
||||
where T : class, ISurfaceEntryCollector
|
||||
{
|
||||
services.AddSingleton<ISurfaceEntryCollector, T>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>Adds an entry point collector.</summary>
|
||||
public static IServiceCollection AddEntryPointCollector<T>(this IServiceCollection services)
|
||||
where T : class, IEntryPointCollector
|
||||
{
|
||||
services.AddSingleton<IEntryPointCollector, T>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user