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:
@@ -0,0 +1,361 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// .NET semantic adapter for inferring intent and capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 11).
|
||||
/// Detects ASP.NET Core, Console apps, Worker services, Azure Functions.
|
||||
/// </remarks>
|
||||
public sealed class DotNetSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
{
|
||||
public IReadOnlyList<string> SupportedLanguages => ["dotnet", "csharp", "fsharp"];
|
||||
public int Priority => 100;
|
||||
|
||||
private static readonly FrozenDictionary<string, ApplicationIntent> PackageIntentMap = new Dictionary<string, ApplicationIntent>
|
||||
{
|
||||
// ASP.NET Core
|
||||
["Microsoft.AspNetCore"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.App"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.Mvc"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.Mvc.Core"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.Server.Kestrel"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.SignalR"] = ApplicationIntent.WebServer,
|
||||
["Microsoft.AspNetCore.Blazor"] = ApplicationIntent.WebServer,
|
||||
|
||||
// Minimal APIs (ASP.NET Core 6+)
|
||||
["Microsoft.AspNetCore.OpenApi"] = ApplicationIntent.WebServer,
|
||||
["Swashbuckle.AspNetCore"] = ApplicationIntent.WebServer,
|
||||
|
||||
// Workers
|
||||
["Microsoft.Extensions.Hosting"] = ApplicationIntent.Worker,
|
||||
["Microsoft.Extensions.Hosting.WindowsServices"] = ApplicationIntent.Daemon,
|
||||
["Microsoft.Extensions.Hosting.Systemd"] = ApplicationIntent.Daemon,
|
||||
|
||||
// Serverless
|
||||
["Microsoft.Azure.Functions.Worker"] = ApplicationIntent.Serverless,
|
||||
["Microsoft.Azure.WebJobs"] = ApplicationIntent.Serverless,
|
||||
["Amazon.Lambda.Core"] = ApplicationIntent.Serverless,
|
||||
["Amazon.Lambda.AspNetCoreServer"] = ApplicationIntent.Serverless,
|
||||
["Google.Cloud.Functions.Framework"] = ApplicationIntent.Serverless,
|
||||
|
||||
// gRPC
|
||||
["Grpc.AspNetCore"] = ApplicationIntent.RpcServer,
|
||||
["Grpc.Core"] = ApplicationIntent.RpcServer,
|
||||
["Grpc.Net.Client"] = ApplicationIntent.RpcServer,
|
||||
|
||||
// GraphQL
|
||||
["HotChocolate.AspNetCore"] = ApplicationIntent.GraphQlServer,
|
||||
["GraphQL.Server.Core"] = ApplicationIntent.GraphQlServer,
|
||||
|
||||
// Message queues / workers
|
||||
["MassTransit"] = ApplicationIntent.Worker,
|
||||
["NServiceBus"] = ApplicationIntent.Worker,
|
||||
["Rebus"] = ApplicationIntent.Worker,
|
||||
["Azure.Messaging.ServiceBus"] = ApplicationIntent.Worker,
|
||||
["RabbitMQ.Client"] = ApplicationIntent.Worker,
|
||||
["Confluent.Kafka"] = ApplicationIntent.StreamProcessor,
|
||||
|
||||
// Schedulers
|
||||
["Hangfire"] = ApplicationIntent.ScheduledTask,
|
||||
["Quartz"] = ApplicationIntent.ScheduledTask,
|
||||
|
||||
// CLI
|
||||
["System.CommandLine"] = ApplicationIntent.CliTool,
|
||||
["McMaster.Extensions.CommandLineUtils"] = ApplicationIntent.CliTool,
|
||||
["CommandLineParser"] = ApplicationIntent.CliTool,
|
||||
["Spectre.Console.Cli"] = ApplicationIntent.CliTool,
|
||||
|
||||
// Testing
|
||||
["Microsoft.NET.Test.Sdk"] = ApplicationIntent.TestRunner,
|
||||
["xunit"] = ApplicationIntent.TestRunner,
|
||||
["NUnit"] = ApplicationIntent.TestRunner,
|
||||
["MSTest.TestFramework"] = ApplicationIntent.TestRunner,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static readonly FrozenDictionary<string, CapabilityClass> PackageCapabilityMap = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
// Network
|
||||
["System.Net.Http"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["System.Net.Sockets"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["RestSharp"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["Refit"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["Flurl.Http"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
|
||||
// Databases
|
||||
["Microsoft.EntityFrameworkCore"] = CapabilityClass.DatabaseSql,
|
||||
["Npgsql"] = CapabilityClass.DatabaseSql,
|
||||
["MySql.Data"] = CapabilityClass.DatabaseSql,
|
||||
["Microsoft.Data.SqlClient"] = CapabilityClass.DatabaseSql,
|
||||
["System.Data.SqlClient"] = CapabilityClass.DatabaseSql,
|
||||
["Oracle.ManagedDataAccess"] = CapabilityClass.DatabaseSql,
|
||||
["Dapper"] = CapabilityClass.DatabaseSql,
|
||||
["MongoDB.Driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["Cassandra.Driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["StackExchange.Redis"] = CapabilityClass.CacheAccess,
|
||||
["Microsoft.Extensions.Caching.StackExchangeRedis"] = CapabilityClass.CacheAccess,
|
||||
["Microsoft.Extensions.Caching.Memory"] = CapabilityClass.CacheAccess,
|
||||
|
||||
// Message queues
|
||||
["RabbitMQ.Client"] = CapabilityClass.MessageQueue,
|
||||
["Azure.Messaging.ServiceBus"] = CapabilityClass.MessageQueue,
|
||||
["Confluent.Kafka"] = CapabilityClass.MessageQueue,
|
||||
["NATS.Client"] = CapabilityClass.MessageQueue,
|
||||
|
||||
// File operations
|
||||
["System.IO.FileSystem"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["System.IO.Compression"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
|
||||
// Process
|
||||
["System.Diagnostics.Process"] = CapabilityClass.ProcessSpawn,
|
||||
["CliWrap"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
|
||||
// Crypto
|
||||
["System.Security.Cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["BouncyCastle.Cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["NSec.Cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
|
||||
// Cloud SDKs
|
||||
["AWSSDK.Core"] = CapabilityClass.CloudSdk,
|
||||
["AWSSDK.S3"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["Azure.Storage.Blobs"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["Google.Cloud.Storage.V1"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
|
||||
// Serialization
|
||||
["Newtonsoft.Json"] = CapabilityClass.UnsafeDeserialization,
|
||||
["System.Text.Json"] = CapabilityClass.UnsafeDeserialization,
|
||||
["YamlDotNet"] = CapabilityClass.UnsafeDeserialization,
|
||||
["System.Xml"] = CapabilityClass.XmlExternalEntities,
|
||||
["System.Xml.Linq"] = CapabilityClass.XmlExternalEntities,
|
||||
|
||||
// Template engines
|
||||
["RazorLight"] = CapabilityClass.TemplateRendering,
|
||||
["Scriban"] = CapabilityClass.TemplateRendering,
|
||||
["Fluid"] = CapabilityClass.TemplateRendering,
|
||||
|
||||
// Dynamic code
|
||||
["Microsoft.CodeAnalysis.CSharp.Scripting"] = CapabilityClass.DynamicCodeEval,
|
||||
["System.Reflection.Emit"] = CapabilityClass.DynamicCodeEval,
|
||||
|
||||
// Logging/metrics
|
||||
["Serilog"] = CapabilityClass.LogEmit,
|
||||
["NLog"] = CapabilityClass.LogEmit,
|
||||
["Microsoft.Extensions.Logging"] = CapabilityClass.LogEmit,
|
||||
["App.Metrics"] = CapabilityClass.MetricsEmit,
|
||||
["prometheus-net"] = CapabilityClass.MetricsEmit,
|
||||
["OpenTelemetry"] = CapabilityClass.TracingEmit | CapabilityClass.MetricsEmit,
|
||||
|
||||
// Auth
|
||||
["Microsoft.AspNetCore.Authentication"] = CapabilityClass.Authentication,
|
||||
["Microsoft.AspNetCore.Authorization"] = CapabilityClass.Authorization,
|
||||
["Microsoft.AspNetCore.Identity"] = CapabilityClass.Authentication | CapabilityClass.SessionManagement,
|
||||
["IdentityServer4"] = CapabilityClass.Authentication | CapabilityClass.Authorization,
|
||||
["Duende.IdentityServer"] = CapabilityClass.Authentication | CapabilityClass.Authorization,
|
||||
|
||||
// Secrets
|
||||
["Azure.Security.KeyVault.Secrets"] = CapabilityClass.SecretAccess,
|
||||
["VaultSharp"] = CapabilityClass.SecretAccess,
|
||||
["Microsoft.Extensions.Configuration"] = CapabilityClass.ConfigLoad | CapabilityClass.EnvironmentRead,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public async Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new SemanticEntrypointBuilder()
|
||||
.WithId(GenerateId(context))
|
||||
.WithSpecification(context.Specification)
|
||||
.WithLanguage("dotnet");
|
||||
|
||||
var reasoningChain = new List<string>();
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
if (context.Dependencies.TryGetValue("dotnet", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
if (PackageIntentMap.TryGetValue(normalizedDep, out var mappedIntent))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Package {dep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = cmdSignals.Intent;
|
||||
reasoningChain.Add($"Command pattern -> {intent}");
|
||||
}
|
||||
|
||||
foreach (var cap in GetCapabilityFlags(cmdSignals.Capabilities))
|
||||
{
|
||||
builder.AddCapability(cap);
|
||||
}
|
||||
|
||||
// Check for P/Invoke usage
|
||||
if (await HasPInvokeUsageAsync(context, cancellationToken))
|
||||
{
|
||||
builder.AddCapability(CapabilityClass.SystemPrivileged);
|
||||
reasoningChain.Add("P/Invoke usage detected -> SystemPrivileged");
|
||||
}
|
||||
|
||||
// Check exposed ports
|
||||
if (context.Specification.ExposedPorts.Length > 0)
|
||||
{
|
||||
var webPorts = context.Specification.ExposedPorts.Where(IsWebPort).ToList();
|
||||
if (webPorts.Count > 0 && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add($"Exposed web ports: {string.Join(", ", webPorts)}");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
// Check environment variables for ASP.NET patterns
|
||||
if (context.Specification.Environment?.ContainsKey("ASPNETCORE_URLS") == true)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add("ASPNETCORE_URLS environment variable -> WebServer");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
var confidence = DetermineConfidence(reasoningChain, intent, framework);
|
||||
|
||||
builder.WithIntent(intent)
|
||||
.WithConfidence(confidence);
|
||||
|
||||
if (framework is not null)
|
||||
{
|
||||
builder.WithFramework(framework);
|
||||
}
|
||||
|
||||
return await Task.FromResult(builder.Build());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle NuGet package references with versions
|
||||
var parts = dep.Split('/');
|
||||
return parts[0].Trim();
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
var priorityOrder = new[]
|
||||
{
|
||||
ApplicationIntent.Unknown,
|
||||
ApplicationIntent.TestRunner,
|
||||
ApplicationIntent.CliTool,
|
||||
ApplicationIntent.BatchJob,
|
||||
ApplicationIntent.Worker,
|
||||
ApplicationIntent.Daemon,
|
||||
ApplicationIntent.ScheduledTask,
|
||||
ApplicationIntent.StreamProcessor,
|
||||
ApplicationIntent.Serverless,
|
||||
ApplicationIntent.WebServer,
|
||||
ApplicationIntent.RpcServer,
|
||||
ApplicationIntent.GraphQlServer,
|
||||
};
|
||||
|
||||
return Array.IndexOf(priorityOrder, newer) > Array.IndexOf(priorityOrder, current);
|
||||
}
|
||||
|
||||
private static (ApplicationIntent Intent, CapabilityClass Capabilities) AnalyzeCommand(EntrypointSpecification spec)
|
||||
{
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var caps = CapabilityClass.None;
|
||||
|
||||
// dotnet run with web project
|
||||
if (cmd.Contains("dotnet") && cmd.Contains("run"))
|
||||
{
|
||||
// Could be anything - need more signals
|
||||
}
|
||||
// dotnet test
|
||||
else if (cmd.Contains("dotnet") && cmd.Contains("test"))
|
||||
{
|
||||
intent = ApplicationIntent.TestRunner;
|
||||
}
|
||||
// Published executable
|
||||
else if (cmd.EndsWith(".dll") || !cmd.Contains("dotnet"))
|
||||
{
|
||||
// Self-contained - intent depends on other signals
|
||||
caps |= CapabilityClass.FileExecute;
|
||||
}
|
||||
|
||||
return (intent, caps);
|
||||
}
|
||||
|
||||
private static async Task<bool> HasPInvokeUsageAsync(SemanticAnalysisContext context, CancellationToken ct)
|
||||
{
|
||||
// Check for native libraries
|
||||
var nativePaths = new[] { "/app", "/lib", "/usr/lib" };
|
||||
foreach (var path in nativePaths)
|
||||
{
|
||||
if (await context.FileSystem.DirectoryExistsAsync(path, ct))
|
||||
{
|
||||
var files = await context.FileSystem.ListFilesAsync(path, "*.so", ct);
|
||||
if (files.Any(f => f.Contains("native") || f.Contains("runtimes")))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWebPort(int port)
|
||||
{
|
||||
return port is 80 or 443 or 5000 or 5001 or 8080 or 8443;
|
||||
}
|
||||
|
||||
private static SemanticConfidence DetermineConfidence(List<string> reasoning, ApplicationIntent intent, string? framework)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
if (framework is not null && reasoning.Count >= 3)
|
||||
return SemanticConfidence.High(reasoning.ToArray());
|
||||
|
||||
if (framework is not null)
|
||||
return SemanticConfidence.Medium(reasoning.ToArray());
|
||||
|
||||
return SemanticConfidence.Low(reasoning.ToArray());
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateId(SemanticAnalysisContext context)
|
||||
{
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-dotnet-{hash[..12]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Go semantic adapter for inferring intent and capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 12).
|
||||
/// Detects net/http patterns, cobra/urfave CLI, gRPC servers, main package analysis.
|
||||
/// </remarks>
|
||||
public sealed class GoSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
{
|
||||
public IReadOnlyList<string> SupportedLanguages => ["go", "golang"];
|
||||
public int Priority => 100;
|
||||
|
||||
private static readonly FrozenDictionary<string, ApplicationIntent> ModuleIntentMap = new Dictionary<string, ApplicationIntent>
|
||||
{
|
||||
// Web frameworks
|
||||
["net/http"] = ApplicationIntent.WebServer,
|
||||
["github.com/gin-gonic/gin"] = ApplicationIntent.WebServer,
|
||||
["github.com/labstack/echo"] = ApplicationIntent.WebServer,
|
||||
["github.com/gofiber/fiber"] = ApplicationIntent.WebServer,
|
||||
["github.com/gorilla/mux"] = ApplicationIntent.WebServer,
|
||||
["github.com/go-chi/chi"] = ApplicationIntent.WebServer,
|
||||
["github.com/julienschmidt/httprouter"] = ApplicationIntent.WebServer,
|
||||
["github.com/valyala/fasthttp"] = ApplicationIntent.WebServer,
|
||||
["github.com/beego/beego"] = ApplicationIntent.WebServer,
|
||||
["github.com/revel/revel"] = ApplicationIntent.WebServer,
|
||||
["github.com/go-martini/martini"] = ApplicationIntent.WebServer,
|
||||
|
||||
// CLI frameworks
|
||||
["github.com/spf13/cobra"] = ApplicationIntent.CliTool,
|
||||
["github.com/urfave/cli"] = ApplicationIntent.CliTool,
|
||||
["github.com/alecthomas/kingpin"] = ApplicationIntent.CliTool,
|
||||
["github.com/jessevdk/go-flags"] = ApplicationIntent.CliTool,
|
||||
["github.com/peterbourgon/ff"] = ApplicationIntent.CliTool,
|
||||
|
||||
// gRPC
|
||||
["google.golang.org/grpc"] = ApplicationIntent.RpcServer,
|
||||
["github.com/grpc-ecosystem/grpc-gateway"] = ApplicationIntent.RpcServer,
|
||||
|
||||
// GraphQL
|
||||
["github.com/graphql-go/graphql"] = ApplicationIntent.GraphQlServer,
|
||||
["github.com/99designs/gqlgen"] = ApplicationIntent.GraphQlServer,
|
||||
["github.com/graph-gophers/graphql-go"] = ApplicationIntent.GraphQlServer,
|
||||
|
||||
// Workers/queues
|
||||
["github.com/hibiken/asynq"] = ApplicationIntent.Worker,
|
||||
["github.com/gocraft/work"] = ApplicationIntent.Worker,
|
||||
["github.com/Shopify/sarama"] = ApplicationIntent.StreamProcessor,
|
||||
["github.com/confluentinc/confluent-kafka-go"] = ApplicationIntent.StreamProcessor,
|
||||
["github.com/segmentio/kafka-go"] = ApplicationIntent.StreamProcessor,
|
||||
["github.com/nats-io/nats.go"] = ApplicationIntent.MessageBroker,
|
||||
["github.com/streadway/amqp"] = ApplicationIntent.Worker,
|
||||
["github.com/rabbitmq/amqp091-go"] = ApplicationIntent.Worker,
|
||||
|
||||
// Serverless
|
||||
["github.com/aws/aws-lambda-go"] = ApplicationIntent.Serverless,
|
||||
["cloud.google.com/go/functions"] = ApplicationIntent.Serverless,
|
||||
|
||||
// Schedulers
|
||||
["github.com/robfig/cron"] = ApplicationIntent.ScheduledTask,
|
||||
["github.com/go-co-op/gocron"] = ApplicationIntent.ScheduledTask,
|
||||
|
||||
// Proxy/Gateway
|
||||
["github.com/envoyproxy/go-control-plane"] = ApplicationIntent.ProxyGateway,
|
||||
["github.com/traefik/traefik"] = ApplicationIntent.ProxyGateway,
|
||||
|
||||
// Metrics/monitoring
|
||||
["github.com/prometheus/client_golang"] = ApplicationIntent.MetricsCollector,
|
||||
|
||||
// Container agents
|
||||
["k8s.io/client-go"] = ApplicationIntent.ContainerAgent,
|
||||
["sigs.k8s.io/controller-runtime"] = ApplicationIntent.ContainerAgent,
|
||||
|
||||
// Testing
|
||||
["testing"] = ApplicationIntent.TestRunner,
|
||||
["github.com/stretchr/testify"] = ApplicationIntent.TestRunner,
|
||||
["github.com/onsi/ginkgo"] = ApplicationIntent.TestRunner,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static readonly FrozenDictionary<string, CapabilityClass> ModuleCapabilityMap = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
// Network
|
||||
["net"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["net/http"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["golang.org/x/net"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["github.com/valyala/fasthttp"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
|
||||
// DNS
|
||||
["net/dns"] = CapabilityClass.NetworkDns,
|
||||
|
||||
// File system
|
||||
["os"] = CapabilityClass.FileRead | CapabilityClass.FileWrite | CapabilityClass.EnvironmentRead,
|
||||
["io"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["io/ioutil"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["path/filepath"] = CapabilityClass.FileRead,
|
||||
["github.com/fsnotify/fsnotify"] = CapabilityClass.FileWatch,
|
||||
|
||||
// Process
|
||||
["os/exec"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["os/signal"] = CapabilityClass.ProcessSignal,
|
||||
["syscall"] = CapabilityClass.SystemPrivileged,
|
||||
["golang.org/x/sys"] = CapabilityClass.SystemPrivileged,
|
||||
|
||||
// Databases
|
||||
["database/sql"] = CapabilityClass.DatabaseSql,
|
||||
["github.com/lib/pq"] = CapabilityClass.DatabaseSql,
|
||||
["github.com/go-sql-driver/mysql"] = CapabilityClass.DatabaseSql,
|
||||
["github.com/jackc/pgx"] = CapabilityClass.DatabaseSql,
|
||||
["github.com/jmoiron/sqlx"] = CapabilityClass.DatabaseSql,
|
||||
["gorm.io/gorm"] = CapabilityClass.DatabaseSql,
|
||||
["go.mongodb.org/mongo-driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["github.com/gocql/gocql"] = CapabilityClass.DatabaseNoSql,
|
||||
["github.com/go-redis/redis"] = CapabilityClass.CacheAccess,
|
||||
["github.com/redis/go-redis"] = CapabilityClass.CacheAccess,
|
||||
["github.com/bradfitz/gomemcache"] = CapabilityClass.CacheAccess,
|
||||
["github.com/allegro/bigcache"] = CapabilityClass.CacheAccess,
|
||||
|
||||
// Message queues
|
||||
["github.com/streadway/amqp"] = CapabilityClass.MessageQueue,
|
||||
["github.com/rabbitmq/amqp091-go"] = CapabilityClass.MessageQueue,
|
||||
["github.com/Shopify/sarama"] = CapabilityClass.MessageQueue,
|
||||
["github.com/nats-io/nats.go"] = CapabilityClass.MessageQueue,
|
||||
|
||||
// Crypto
|
||||
["crypto"] = CapabilityClass.CryptoEncrypt,
|
||||
["crypto/tls"] = CapabilityClass.CryptoEncrypt,
|
||||
["crypto/rsa"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["crypto/ecdsa"] = CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["crypto/ed25519"] = CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["golang.org/x/crypto"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
|
||||
// Cloud SDKs
|
||||
["github.com/aws/aws-sdk-go"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["github.com/aws/aws-sdk-go-v2"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["cloud.google.com/go"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["github.com/Azure/azure-sdk-for-go"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
|
||||
// Serialization
|
||||
["encoding/json"] = CapabilityClass.UnsafeDeserialization,
|
||||
["encoding/gob"] = CapabilityClass.UnsafeDeserialization,
|
||||
["encoding/xml"] = CapabilityClass.XmlExternalEntities,
|
||||
["github.com/vmihailenco/msgpack"] = CapabilityClass.UnsafeDeserialization,
|
||||
|
||||
// Template engines
|
||||
["text/template"] = CapabilityClass.TemplateRendering,
|
||||
["html/template"] = CapabilityClass.TemplateRendering,
|
||||
|
||||
// Dynamic code
|
||||
["reflect"] = CapabilityClass.DynamicCodeEval,
|
||||
["plugin"] = CapabilityClass.DynamicCodeEval,
|
||||
|
||||
// Logging
|
||||
["log"] = CapabilityClass.LogEmit,
|
||||
["github.com/sirupsen/logrus"] = CapabilityClass.LogEmit,
|
||||
["go.uber.org/zap"] = CapabilityClass.LogEmit,
|
||||
["github.com/rs/zerolog"] = CapabilityClass.LogEmit,
|
||||
|
||||
// Metrics/tracing
|
||||
["github.com/prometheus/client_golang"] = CapabilityClass.MetricsEmit,
|
||||
["go.opentelemetry.io/otel"] = CapabilityClass.TracingEmit | CapabilityClass.MetricsEmit,
|
||||
|
||||
// Auth
|
||||
["github.com/golang-jwt/jwt"] = CapabilityClass.Authentication | CapabilityClass.SessionManagement,
|
||||
["github.com/coreos/go-oidc"] = CapabilityClass.Authentication,
|
||||
["golang.org/x/oauth2"] = CapabilityClass.Authentication,
|
||||
|
||||
// Secrets
|
||||
["github.com/hashicorp/vault/api"] = CapabilityClass.SecretAccess,
|
||||
|
||||
// Container/system
|
||||
["github.com/containerd/containerd"] = CapabilityClass.ContainerEscape,
|
||||
["github.com/docker/docker"] = CapabilityClass.ContainerEscape,
|
||||
["github.com/opencontainers/runc"] = CapabilityClass.ContainerEscape,
|
||||
["k8s.io/client-go"] = CapabilityClass.SystemPrivileged,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public async Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new SemanticEntrypointBuilder()
|
||||
.WithId(GenerateId(context))
|
||||
.WithSpecification(context.Specification)
|
||||
.WithLanguage("go");
|
||||
|
||||
var reasoningChain = new List<string>();
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies (go.mod imports)
|
||||
if (context.Dependencies.TryGetValue("go", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
if (ModuleIntentMap.TryGetValue(normalizedDep, out var mappedIntent))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
}
|
||||
}
|
||||
|
||||
if (ModuleCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Module {dep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = cmdSignals.Intent;
|
||||
reasoningChain.Add($"Command pattern -> {intent}");
|
||||
}
|
||||
|
||||
foreach (var cap in GetCapabilityFlags(cmdSignals.Capabilities))
|
||||
{
|
||||
builder.AddCapability(cap);
|
||||
}
|
||||
|
||||
// Check for CGO usage
|
||||
if (await HasCgoUsageAsync(context, cancellationToken))
|
||||
{
|
||||
builder.AddCapability(CapabilityClass.SystemPrivileged);
|
||||
reasoningChain.Add("CGO usage detected -> SystemPrivileged");
|
||||
}
|
||||
|
||||
// Check exposed ports
|
||||
if (context.Specification.ExposedPorts.Length > 0)
|
||||
{
|
||||
var webPorts = context.Specification.ExposedPorts.Where(IsWebPort).ToList();
|
||||
if (webPorts.Count > 0 && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add($"Exposed web ports: {string.Join(", ", webPorts)}");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
var confidence = DetermineConfidence(reasoningChain, intent, framework);
|
||||
|
||||
builder.WithIntent(intent)
|
||||
.WithConfidence(confidence);
|
||||
|
||||
if (framework is not null)
|
||||
{
|
||||
builder.WithFramework(framework);
|
||||
}
|
||||
|
||||
return await Task.FromResult(builder.Build());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle Go module paths with versions
|
||||
var parts = dep.Split('@');
|
||||
return parts[0].Trim();
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
var priorityOrder = new[]
|
||||
{
|
||||
ApplicationIntent.Unknown,
|
||||
ApplicationIntent.TestRunner,
|
||||
ApplicationIntent.CliTool,
|
||||
ApplicationIntent.BatchJob,
|
||||
ApplicationIntent.Worker,
|
||||
ApplicationIntent.ScheduledTask,
|
||||
ApplicationIntent.StreamProcessor,
|
||||
ApplicationIntent.MessageBroker,
|
||||
ApplicationIntent.Serverless,
|
||||
ApplicationIntent.ProxyGateway,
|
||||
ApplicationIntent.WebServer,
|
||||
ApplicationIntent.RpcServer,
|
||||
ApplicationIntent.GraphQlServer,
|
||||
ApplicationIntent.ContainerAgent,
|
||||
};
|
||||
|
||||
return Array.IndexOf(priorityOrder, newer) > Array.IndexOf(priorityOrder, current);
|
||||
}
|
||||
|
||||
private static (ApplicationIntent Intent, CapabilityClass Capabilities) AnalyzeCommand(EntrypointSpecification spec)
|
||||
{
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var caps = CapabilityClass.None;
|
||||
|
||||
// Go binaries are typically single executables
|
||||
// Check for common patterns
|
||||
if (cmd.Contains("serve") || cmd.Contains("server"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
else if (cmd.Contains("worker") || cmd.Contains("consume"))
|
||||
{
|
||||
intent = ApplicationIntent.Worker;
|
||||
caps |= CapabilityClass.MessageQueue;
|
||||
}
|
||||
else if (cmd.Contains("migrate") || cmd.Contains("seed"))
|
||||
{
|
||||
intent = ApplicationIntent.BatchJob;
|
||||
caps |= CapabilityClass.DatabaseSql;
|
||||
}
|
||||
|
||||
return (intent, caps);
|
||||
}
|
||||
|
||||
private static async Task<bool> HasCgoUsageAsync(SemanticAnalysisContext context, CancellationToken ct)
|
||||
{
|
||||
// Check for C libraries in common locations
|
||||
var libPaths = new[] { "/lib", "/usr/lib", "/usr/local/lib" };
|
||||
foreach (var path in libPaths)
|
||||
{
|
||||
if (await context.FileSystem.DirectoryExistsAsync(path, ct))
|
||||
{
|
||||
var files = await context.FileSystem.ListFilesAsync(path, "*.so*", ct);
|
||||
if (files.Any())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWebPort(int port)
|
||||
{
|
||||
return port is 80 or 443 or 8080 or 8443 or 9000 or 3000;
|
||||
}
|
||||
|
||||
private static SemanticConfidence DetermineConfidence(List<string> reasoning, ApplicationIntent intent, string? framework)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
if (framework is not null && reasoning.Count >= 3)
|
||||
return SemanticConfidence.High(reasoning.ToArray());
|
||||
|
||||
if (framework is not null)
|
||||
return SemanticConfidence.Medium(reasoning.ToArray());
|
||||
|
||||
return SemanticConfidence.Low(reasoning.ToArray());
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateId(SemanticAnalysisContext context)
|
||||
{
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-go-{hash[..12]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Java semantic adapter for inferring intent and capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 9).
|
||||
/// Detects Spring Boot, Quarkus, Micronaut, Kafka Streams, Main-Class patterns.
|
||||
/// </remarks>
|
||||
public sealed class JavaSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
{
|
||||
public IReadOnlyList<string> SupportedLanguages => ["java", "kotlin", "scala"];
|
||||
public int Priority => 100;
|
||||
|
||||
private static readonly FrozenDictionary<string, ApplicationIntent> FrameworkIntentMap = new Dictionary<string, ApplicationIntent>
|
||||
{
|
||||
// Spring ecosystem
|
||||
["spring-boot"] = ApplicationIntent.WebServer,
|
||||
["spring-boot-starter-web"] = ApplicationIntent.WebServer,
|
||||
["spring-boot-starter-webflux"] = ApplicationIntent.WebServer,
|
||||
["spring-cloud-function"] = ApplicationIntent.Serverless,
|
||||
["spring-kafka"] = ApplicationIntent.StreamProcessor,
|
||||
["spring-amqp"] = ApplicationIntent.Worker,
|
||||
["spring-batch"] = ApplicationIntent.BatchJob,
|
||||
|
||||
// Microframeworks
|
||||
["quarkus"] = ApplicationIntent.WebServer,
|
||||
["quarkus-resteasy"] = ApplicationIntent.WebServer,
|
||||
["micronaut"] = ApplicationIntent.WebServer,
|
||||
["micronaut-http-server"] = ApplicationIntent.WebServer,
|
||||
["helidon"] = ApplicationIntent.WebServer,
|
||||
["dropwizard"] = ApplicationIntent.WebServer,
|
||||
["jersey"] = ApplicationIntent.WebServer,
|
||||
["javalin"] = ApplicationIntent.WebServer,
|
||||
["spark-java"] = ApplicationIntent.WebServer,
|
||||
["vertx-web"] = ApplicationIntent.WebServer,
|
||||
|
||||
// Workers/queues
|
||||
["kafka-streams"] = ApplicationIntent.StreamProcessor,
|
||||
["kafka-clients"] = ApplicationIntent.Worker,
|
||||
["activemq"] = ApplicationIntent.Worker,
|
||||
["rabbitmq-client"] = ApplicationIntent.Worker,
|
||||
|
||||
// CLI
|
||||
["picocli"] = ApplicationIntent.CliTool,
|
||||
["jcommander"] = ApplicationIntent.CliTool,
|
||||
["commons-cli"] = ApplicationIntent.CliTool,
|
||||
|
||||
// Serverless
|
||||
["aws-lambda-java"] = ApplicationIntent.Serverless,
|
||||
["aws-lambda-java-core"] = ApplicationIntent.Serverless,
|
||||
["azure-functions-java"] = ApplicationIntent.Serverless,
|
||||
["functions-framework-java"] = ApplicationIntent.Serverless,
|
||||
|
||||
// gRPC
|
||||
["grpc-java"] = ApplicationIntent.RpcServer,
|
||||
["grpc-netty"] = ApplicationIntent.RpcServer,
|
||||
["grpc-stub"] = ApplicationIntent.RpcServer,
|
||||
|
||||
// GraphQL
|
||||
["graphql-java"] = ApplicationIntent.GraphQlServer,
|
||||
["netflix-dgs"] = ApplicationIntent.GraphQlServer,
|
||||
["graphql-spring-boot"] = ApplicationIntent.GraphQlServer,
|
||||
|
||||
// Database servers (when running as embedded)
|
||||
["h2"] = ApplicationIntent.DatabaseServer,
|
||||
["derby"] = ApplicationIntent.DatabaseServer,
|
||||
|
||||
// Testing
|
||||
["junit"] = ApplicationIntent.TestRunner,
|
||||
["testng"] = ApplicationIntent.TestRunner,
|
||||
["mockito"] = ApplicationIntent.TestRunner,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static readonly FrozenDictionary<string, CapabilityClass> DependencyCapabilityMap = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
// Network
|
||||
["netty"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["okhttp"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["apache-httpclient"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["jersey-client"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["retrofit"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["feign"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
|
||||
// Databases
|
||||
["jdbc"] = CapabilityClass.DatabaseSql,
|
||||
["postgresql"] = CapabilityClass.DatabaseSql,
|
||||
["mysql-connector"] = CapabilityClass.DatabaseSql,
|
||||
["ojdbc"] = CapabilityClass.DatabaseSql,
|
||||
["mssql-jdbc"] = CapabilityClass.DatabaseSql,
|
||||
["hibernate"] = CapabilityClass.DatabaseSql,
|
||||
["jpa"] = CapabilityClass.DatabaseSql,
|
||||
["mybatis"] = CapabilityClass.DatabaseSql,
|
||||
["jooq"] = CapabilityClass.DatabaseSql,
|
||||
["mongo-java-driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["cassandra-driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["jedis"] = CapabilityClass.CacheAccess,
|
||||
["lettuce"] = CapabilityClass.CacheAccess,
|
||||
["redisson"] = CapabilityClass.CacheAccess,
|
||||
["ehcache"] = CapabilityClass.CacheAccess,
|
||||
["caffeine"] = CapabilityClass.CacheAccess,
|
||||
|
||||
// Message queues
|
||||
["jms"] = CapabilityClass.MessageQueue,
|
||||
["activemq"] = CapabilityClass.MessageQueue,
|
||||
["kafka"] = CapabilityClass.MessageQueue,
|
||||
["rabbitmq"] = CapabilityClass.MessageQueue,
|
||||
|
||||
// File operations
|
||||
["commons-io"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["java.nio.file"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
|
||||
// Process
|
||||
["processbuilder"] = CapabilityClass.ProcessSpawn,
|
||||
["runtime.exec"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
|
||||
// Crypto
|
||||
["bouncycastle"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
["jasypt"] = CapabilityClass.CryptoEncrypt,
|
||||
["tink"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
|
||||
// Cloud SDKs
|
||||
["aws-sdk-java"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["google-cloud-java"] = CapabilityClass.CloudSdk,
|
||||
["azure-sdk"] = CapabilityClass.CloudSdk,
|
||||
|
||||
// Serialization (potentially unsafe)
|
||||
["jackson"] = CapabilityClass.UnsafeDeserialization,
|
||||
["gson"] = CapabilityClass.UnsafeDeserialization,
|
||||
["xstream"] = CapabilityClass.UnsafeDeserialization | CapabilityClass.XmlExternalEntities,
|
||||
["fastjson"] = CapabilityClass.UnsafeDeserialization,
|
||||
["kryo"] = CapabilityClass.UnsafeDeserialization,
|
||||
["java.io.objectinputstream"] = CapabilityClass.UnsafeDeserialization,
|
||||
|
||||
// XML
|
||||
["dom4j"] = CapabilityClass.XmlExternalEntities,
|
||||
["jdom"] = CapabilityClass.XmlExternalEntities,
|
||||
["woodstox"] = CapabilityClass.XmlExternalEntities,
|
||||
|
||||
// Template engines
|
||||
["thymeleaf"] = CapabilityClass.TemplateRendering,
|
||||
["freemarker"] = CapabilityClass.TemplateRendering,
|
||||
["velocity"] = CapabilityClass.TemplateRendering,
|
||||
["pebble"] = CapabilityClass.TemplateRendering,
|
||||
|
||||
// Logging
|
||||
["slf4j"] = CapabilityClass.LogEmit,
|
||||
["log4j"] = CapabilityClass.LogEmit,
|
||||
["logback"] = CapabilityClass.LogEmit,
|
||||
|
||||
// Metrics
|
||||
["micrometer"] = CapabilityClass.MetricsEmit,
|
||||
["prometheus"] = CapabilityClass.MetricsEmit,
|
||||
["opentelemetry"] = CapabilityClass.TracingEmit | CapabilityClass.MetricsEmit,
|
||||
["jaeger"] = CapabilityClass.TracingEmit,
|
||||
["zipkin"] = CapabilityClass.TracingEmit,
|
||||
|
||||
// Auth
|
||||
["spring-security"] = CapabilityClass.Authentication | CapabilityClass.Authorization,
|
||||
["shiro"] = CapabilityClass.Authentication | CapabilityClass.Authorization,
|
||||
["jwt"] = CapabilityClass.Authentication | CapabilityClass.SessionManagement,
|
||||
["oauth2"] = CapabilityClass.Authentication,
|
||||
["keycloak"] = CapabilityClass.Authentication | CapabilityClass.Authorization,
|
||||
|
||||
// Secrets
|
||||
["vault-java-driver"] = CapabilityClass.SecretAccess,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public async Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new SemanticEntrypointBuilder()
|
||||
.WithId(GenerateId(context))
|
||||
.WithSpecification(context.Specification)
|
||||
.WithLanguage("java");
|
||||
|
||||
var reasoningChain = new List<string>();
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
if (context.Dependencies.TryGetValue("java", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
if (FrameworkIntentMap.TryGetValue(normalizedDep, out var mappedIntent))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
}
|
||||
}
|
||||
|
||||
if (DependencyCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Dependency {dep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = cmdSignals.Intent;
|
||||
reasoningChain.Add($"Command pattern -> {intent}");
|
||||
}
|
||||
|
||||
foreach (var cap in GetCapabilityFlags(cmdSignals.Capabilities))
|
||||
{
|
||||
builder.AddCapability(cap);
|
||||
}
|
||||
|
||||
// Check for JNI usage
|
||||
if (await HasJniUsageAsync(context, cancellationToken))
|
||||
{
|
||||
builder.AddCapability(CapabilityClass.SystemPrivileged);
|
||||
reasoningChain.Add("JNI usage detected -> SystemPrivileged");
|
||||
}
|
||||
|
||||
// Check exposed ports
|
||||
if (context.Specification.ExposedPorts.Length > 0)
|
||||
{
|
||||
var webPorts = context.Specification.ExposedPorts.Where(IsWebPort).ToList();
|
||||
if (webPorts.Count > 0 && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add($"Exposed web ports: {string.Join(", ", webPorts)}");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
var confidence = DetermineConfidence(reasoningChain, intent, framework);
|
||||
|
||||
builder.WithIntent(intent)
|
||||
.WithConfidence(confidence);
|
||||
|
||||
if (framework is not null)
|
||||
{
|
||||
builder.WithFramework(framework);
|
||||
}
|
||||
|
||||
return await Task.FromResult(builder.Build());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle Maven coordinates (groupId:artifactId:version)
|
||||
var parts = dep.Split(':');
|
||||
var artifactId = parts.Length >= 2 ? parts[1] : parts[0];
|
||||
return artifactId.ToLowerInvariant().Replace("_", "-");
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
var priorityOrder = new[]
|
||||
{
|
||||
ApplicationIntent.Unknown,
|
||||
ApplicationIntent.TestRunner,
|
||||
ApplicationIntent.CliTool,
|
||||
ApplicationIntent.BatchJob,
|
||||
ApplicationIntent.Worker,
|
||||
ApplicationIntent.StreamProcessor,
|
||||
ApplicationIntent.Serverless,
|
||||
ApplicationIntent.WebServer,
|
||||
ApplicationIntent.RpcServer,
|
||||
ApplicationIntent.GraphQlServer,
|
||||
};
|
||||
|
||||
return Array.IndexOf(priorityOrder, newer) > Array.IndexOf(priorityOrder, current);
|
||||
}
|
||||
|
||||
private static (ApplicationIntent Intent, CapabilityClass Capabilities) AnalyzeCommand(EntrypointSpecification spec)
|
||||
{
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var caps = CapabilityClass.None;
|
||||
|
||||
// Check for Spring Boot executable JAR
|
||||
if (cmd.Contains("-jar") && (cmd.Contains("spring") || cmd.Contains("boot")))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
// Quarkus runner
|
||||
else if (cmd.Contains("quarkus-run") || cmd.Contains("quarkus.jar"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
// Kafka Streams
|
||||
else if (cmd.Contains("kafka") && cmd.Contains("streams"))
|
||||
{
|
||||
intent = ApplicationIntent.StreamProcessor;
|
||||
caps |= CapabilityClass.MessageQueue;
|
||||
}
|
||||
// Test runners
|
||||
else if (cmd.Contains("junit") || cmd.Contains("testng") || cmd.Contains("surefire"))
|
||||
{
|
||||
intent = ApplicationIntent.TestRunner;
|
||||
}
|
||||
// GraalVM native image
|
||||
else if (cmd.Contains("native-image") || !cmd.Contains("java"))
|
||||
{
|
||||
// Native executable - intent depends on other signals
|
||||
caps |= CapabilityClass.FileExecute;
|
||||
}
|
||||
|
||||
return (intent, caps);
|
||||
}
|
||||
|
||||
private static async Task<bool> HasJniUsageAsync(SemanticAnalysisContext context, CancellationToken ct)
|
||||
{
|
||||
// Check for .so files in common JNI locations
|
||||
var jniPaths = new[] { "/usr/lib", "/lib", "/app/lib", "/opt/app/lib" };
|
||||
foreach (var path in jniPaths)
|
||||
{
|
||||
if (await context.FileSystem.DirectoryExistsAsync(path, ct))
|
||||
{
|
||||
var files = await context.FileSystem.ListFilesAsync(path, "*.so", ct);
|
||||
if (files.Any())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsWebPort(int port)
|
||||
{
|
||||
return port is 80 or 443 or 8080 or 8443 or 9000 or 8081 or 8082;
|
||||
}
|
||||
|
||||
private static SemanticConfidence DetermineConfidence(List<string> reasoning, ApplicationIntent intent, string? framework)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
if (framework is not null && reasoning.Count >= 3)
|
||||
return SemanticConfidence.High(reasoning.ToArray());
|
||||
|
||||
if (framework is not null)
|
||||
return SemanticConfidence.Medium(reasoning.ToArray());
|
||||
|
||||
return SemanticConfidence.Low(reasoning.ToArray());
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateId(SemanticAnalysisContext context)
|
||||
{
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-java-{hash[..12]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Node.js semantic adapter for inferring intent and capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 10).
|
||||
/// Detects Express, Koa, Fastify, CLI bin entries, worker threads, Lambda handlers.
|
||||
/// </remarks>
|
||||
public sealed class NodeSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
{
|
||||
public IReadOnlyList<string> SupportedLanguages => ["node", "javascript", "typescript"];
|
||||
public int Priority => 100;
|
||||
|
||||
private static readonly FrozenDictionary<string, ApplicationIntent> PackageIntentMap = new Dictionary<string, ApplicationIntent>
|
||||
{
|
||||
// Web frameworks
|
||||
["express"] = ApplicationIntent.WebServer,
|
||||
["koa"] = ApplicationIntent.WebServer,
|
||||
["fastify"] = ApplicationIntent.WebServer,
|
||||
["hapi"] = ApplicationIntent.WebServer,
|
||||
["restify"] = ApplicationIntent.WebServer,
|
||||
["polka"] = ApplicationIntent.WebServer,
|
||||
["micro"] = ApplicationIntent.WebServer,
|
||||
["nest"] = ApplicationIntent.WebServer,
|
||||
["@nestjs/core"] = ApplicationIntent.WebServer,
|
||||
["@nestjs/platform-express"] = ApplicationIntent.WebServer,
|
||||
["next"] = ApplicationIntent.WebServer,
|
||||
["nuxt"] = ApplicationIntent.WebServer,
|
||||
["sveltekit"] = ApplicationIntent.WebServer,
|
||||
["remix"] = ApplicationIntent.WebServer,
|
||||
["adonis"] = ApplicationIntent.WebServer,
|
||||
|
||||
// Workers/queues
|
||||
["bull"] = ApplicationIntent.Worker,
|
||||
["bullmq"] = ApplicationIntent.Worker,
|
||||
["agenda"] = ApplicationIntent.Worker,
|
||||
["bee-queue"] = ApplicationIntent.Worker,
|
||||
["kue"] = ApplicationIntent.Worker,
|
||||
|
||||
// CLI
|
||||
["commander"] = ApplicationIntent.CliTool,
|
||||
["yargs"] = ApplicationIntent.CliTool,
|
||||
["meow"] = ApplicationIntent.CliTool,
|
||||
["oclif"] = ApplicationIntent.CliTool,
|
||||
["inquirer"] = ApplicationIntent.CliTool,
|
||||
["vorpal"] = ApplicationIntent.CliTool,
|
||||
["caporal"] = ApplicationIntent.CliTool,
|
||||
|
||||
// Serverless
|
||||
["aws-lambda"] = ApplicationIntent.Serverless,
|
||||
["@aws-sdk/lambda"] = ApplicationIntent.Serverless,
|
||||
["serverless"] = ApplicationIntent.Serverless,
|
||||
["@azure/functions"] = ApplicationIntent.Serverless,
|
||||
["@google-cloud/functions-framework"] = ApplicationIntent.Serverless,
|
||||
|
||||
// gRPC
|
||||
["@grpc/grpc-js"] = ApplicationIntent.RpcServer,
|
||||
["grpc"] = ApplicationIntent.RpcServer,
|
||||
|
||||
// GraphQL
|
||||
["apollo-server"] = ApplicationIntent.GraphQlServer,
|
||||
["@apollo/server"] = ApplicationIntent.GraphQlServer,
|
||||
["graphql-yoga"] = ApplicationIntent.GraphQlServer,
|
||||
["mercurius"] = ApplicationIntent.GraphQlServer,
|
||||
["type-graphql"] = ApplicationIntent.GraphQlServer,
|
||||
|
||||
// Stream processing
|
||||
["kafka-node"] = ApplicationIntent.StreamProcessor,
|
||||
["kafkajs"] = ApplicationIntent.StreamProcessor,
|
||||
|
||||
// Schedulers
|
||||
["node-cron"] = ApplicationIntent.ScheduledTask,
|
||||
["cron"] = ApplicationIntent.ScheduledTask,
|
||||
["node-schedule"] = ApplicationIntent.ScheduledTask,
|
||||
|
||||
// Metrics/monitoring
|
||||
["prom-client"] = ApplicationIntent.MetricsCollector,
|
||||
|
||||
// Proxy
|
||||
["http-proxy"] = ApplicationIntent.ProxyGateway,
|
||||
["http-proxy-middleware"] = ApplicationIntent.ProxyGateway,
|
||||
|
||||
// Testing
|
||||
["jest"] = ApplicationIntent.TestRunner,
|
||||
["mocha"] = ApplicationIntent.TestRunner,
|
||||
["vitest"] = ApplicationIntent.TestRunner,
|
||||
["ava"] = ApplicationIntent.TestRunner,
|
||||
["tap"] = ApplicationIntent.TestRunner,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static readonly FrozenDictionary<string, CapabilityClass> PackageCapabilityMap = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
// Network
|
||||
["axios"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["got"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["node-fetch"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["undici"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["request"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["superagent"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["socket.io"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["ws"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["net"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["dgram"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkRaw,
|
||||
|
||||
// File system
|
||||
["fs-extra"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["graceful-fs"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["glob"] = CapabilityClass.FileRead,
|
||||
["chokidar"] = CapabilityClass.FileWatch,
|
||||
["multer"] = CapabilityClass.FileUpload,
|
||||
["formidable"] = CapabilityClass.FileUpload,
|
||||
["busboy"] = CapabilityClass.FileUpload,
|
||||
|
||||
// Process
|
||||
["child_process"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["execa"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["shelljs"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["cross-spawn"] = CapabilityClass.ProcessSpawn,
|
||||
|
||||
// Databases
|
||||
["pg"] = CapabilityClass.DatabaseSql,
|
||||
["mysql"] = CapabilityClass.DatabaseSql,
|
||||
["mysql2"] = CapabilityClass.DatabaseSql,
|
||||
["mssql"] = CapabilityClass.DatabaseSql,
|
||||
["sqlite3"] = CapabilityClass.DatabaseSql,
|
||||
["better-sqlite3"] = CapabilityClass.DatabaseSql,
|
||||
["sequelize"] = CapabilityClass.DatabaseSql,
|
||||
["typeorm"] = CapabilityClass.DatabaseSql,
|
||||
["prisma"] = CapabilityClass.DatabaseSql,
|
||||
["knex"] = CapabilityClass.DatabaseSql,
|
||||
["drizzle-orm"] = CapabilityClass.DatabaseSql,
|
||||
["mongoose"] = CapabilityClass.DatabaseNoSql,
|
||||
["mongodb"] = CapabilityClass.DatabaseNoSql,
|
||||
["cassandra-driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["redis"] = CapabilityClass.CacheAccess,
|
||||
["ioredis"] = CapabilityClass.CacheAccess,
|
||||
["memcached"] = CapabilityClass.CacheAccess,
|
||||
|
||||
// Message queues
|
||||
["amqplib"] = CapabilityClass.MessageQueue,
|
||||
["kafkajs"] = CapabilityClass.MessageQueue,
|
||||
["sqs-consumer"] = CapabilityClass.MessageQueue,
|
||||
|
||||
// Crypto
|
||||
["crypto"] = CapabilityClass.CryptoEncrypt,
|
||||
["bcrypt"] = CapabilityClass.CryptoEncrypt,
|
||||
["argon2"] = CapabilityClass.CryptoEncrypt,
|
||||
["jose"] = CapabilityClass.CryptoSign | CapabilityClass.CryptoEncrypt,
|
||||
["jsonwebtoken"] = CapabilityClass.CryptoSign,
|
||||
["node-forge"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign | CapabilityClass.CryptoKeyGen,
|
||||
|
||||
// Cloud SDKs
|
||||
["@aws-sdk/client-s3"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["aws-sdk"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["@google-cloud/storage"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["@azure/storage-blob"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
|
||||
// Unsafe patterns
|
||||
["vm"] = CapabilityClass.DynamicCodeEval,
|
||||
["vm2"] = CapabilityClass.DynamicCodeEval,
|
||||
["isolated-vm"] = CapabilityClass.DynamicCodeEval,
|
||||
["serialize-javascript"] = CapabilityClass.UnsafeDeserialization,
|
||||
["node-serialize"] = CapabilityClass.UnsafeDeserialization,
|
||||
["xml2js"] = CapabilityClass.XmlExternalEntities,
|
||||
["fast-xml-parser"] = CapabilityClass.XmlExternalEntities,
|
||||
|
||||
// Template engines
|
||||
["ejs"] = CapabilityClass.TemplateRendering,
|
||||
["pug"] = CapabilityClass.TemplateRendering,
|
||||
["handlebars"] = CapabilityClass.TemplateRendering,
|
||||
["nunjucks"] = CapabilityClass.TemplateRendering,
|
||||
["mustache"] = CapabilityClass.TemplateRendering,
|
||||
|
||||
// Logging/metrics
|
||||
["winston"] = CapabilityClass.LogEmit,
|
||||
["pino"] = CapabilityClass.LogEmit,
|
||||
["bunyan"] = CapabilityClass.LogEmit,
|
||||
["morgan"] = CapabilityClass.LogEmit,
|
||||
["prom-client"] = CapabilityClass.MetricsEmit,
|
||||
["@opentelemetry/sdk-node"] = CapabilityClass.TracingEmit | CapabilityClass.MetricsEmit,
|
||||
|
||||
// Auth
|
||||
["passport"] = CapabilityClass.Authentication,
|
||||
["express-session"] = CapabilityClass.SessionManagement,
|
||||
["cookie-session"] = CapabilityClass.SessionManagement,
|
||||
["helmet"] = CapabilityClass.Authorization,
|
||||
|
||||
// Config/secrets
|
||||
["dotenv"] = CapabilityClass.SecretAccess | CapabilityClass.ConfigLoad | CapabilityClass.EnvironmentRead,
|
||||
["config"] = CapabilityClass.ConfigLoad,
|
||||
["@hashicorp/vault"] = CapabilityClass.SecretAccess,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public async Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new SemanticEntrypointBuilder()
|
||||
.WithId(GenerateId(context))
|
||||
.WithSpecification(context.Specification)
|
||||
.WithLanguage("node");
|
||||
|
||||
var reasoningChain = new List<string>();
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies
|
||||
if (context.Dependencies.TryGetValue("node", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
if (PackageIntentMap.TryGetValue(normalizedDep, out var mappedIntent))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
}
|
||||
}
|
||||
|
||||
if (PackageCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Package {dep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze entrypoint command
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = cmdSignals.Intent;
|
||||
reasoningChain.Add($"Command pattern -> {intent}");
|
||||
}
|
||||
|
||||
foreach (var cap in GetCapabilityFlags(cmdSignals.Capabilities))
|
||||
{
|
||||
builder.AddCapability(cap);
|
||||
}
|
||||
|
||||
// Check package.json for bin entries -> CLI tool
|
||||
if (context.ManifestPaths.TryGetValue("package.json", out var pkgPath))
|
||||
{
|
||||
if (await HasBinEntriesAsync(context, pkgPath, cancellationToken))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.CliTool;
|
||||
reasoningChain.Add("package.json has bin entries -> CliTool");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check exposed ports
|
||||
if (context.Specification.ExposedPorts.Length > 0)
|
||||
{
|
||||
var webPorts = context.Specification.ExposedPorts.Where(IsWebPort).ToList();
|
||||
if (webPorts.Count > 0 && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add($"Exposed web ports: {string.Join(", ", webPorts)}");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
var confidence = DetermineConfidence(reasoningChain, intent, framework);
|
||||
|
||||
builder.WithIntent(intent)
|
||||
.WithConfidence(confidence);
|
||||
|
||||
if (framework is not null)
|
||||
{
|
||||
builder.WithFramework(framework);
|
||||
}
|
||||
|
||||
return await Task.FromResult(builder.Build());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
// Handle scoped packages and versions
|
||||
return dep.ToLowerInvariant()
|
||||
.Split('@')[0] // Remove version
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
var priorityOrder = new[]
|
||||
{
|
||||
ApplicationIntent.Unknown,
|
||||
ApplicationIntent.TestRunner,
|
||||
ApplicationIntent.CliTool,
|
||||
ApplicationIntent.BatchJob,
|
||||
ApplicationIntent.Worker,
|
||||
ApplicationIntent.ScheduledTask,
|
||||
ApplicationIntent.StreamProcessor,
|
||||
ApplicationIntent.Serverless,
|
||||
ApplicationIntent.WebServer,
|
||||
ApplicationIntent.RpcServer,
|
||||
ApplicationIntent.GraphQlServer,
|
||||
};
|
||||
|
||||
return Array.IndexOf(priorityOrder, newer) > Array.IndexOf(priorityOrder, current);
|
||||
}
|
||||
|
||||
private static (ApplicationIntent Intent, CapabilityClass Capabilities) AnalyzeCommand(EntrypointSpecification spec)
|
||||
{
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var caps = CapabilityClass.None;
|
||||
|
||||
// Next.js
|
||||
if (cmd.Contains("next") && cmd.Contains("start"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
// Nuxt
|
||||
else if (cmd.Contains("nuxt") && cmd.Contains("start"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
// NestJS
|
||||
else if (cmd.Contains("nest") && cmd.Contains("start"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
// PM2
|
||||
else if (cmd.Contains("pm2"))
|
||||
{
|
||||
intent = ApplicationIntent.Daemon;
|
||||
caps |= CapabilityClass.ProcessSpawn;
|
||||
}
|
||||
// Node with --inspect
|
||||
else if (cmd.Contains("--inspect"))
|
||||
{
|
||||
intent = ApplicationIntent.DevServer;
|
||||
}
|
||||
// Test runners
|
||||
else if (cmd.Contains("jest") || cmd.Contains("mocha") || cmd.Contains("vitest"))
|
||||
{
|
||||
intent = ApplicationIntent.TestRunner;
|
||||
}
|
||||
// Worker threads
|
||||
else if (cmd.Contains("worker_threads"))
|
||||
{
|
||||
caps |= CapabilityClass.ProcessSpawn;
|
||||
}
|
||||
|
||||
return (intent, caps);
|
||||
}
|
||||
|
||||
private static async Task<bool> HasBinEntriesAsync(SemanticAnalysisContext context, string pkgPath, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var content = await context.FileSystem.ReadFileAsync(pkgPath, ct);
|
||||
return content.Contains("\"bin\"");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWebPort(int port)
|
||||
{
|
||||
return port is 80 or 443 or 3000 or 3001 or 8000 or 8080 or 8443 or 9000 or 4000;
|
||||
}
|
||||
|
||||
private static SemanticConfidence DetermineConfidence(List<string> reasoning, ApplicationIntent intent, string? framework)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
if (framework is not null && reasoning.Count >= 3)
|
||||
return SemanticConfidence.High(reasoning.ToArray());
|
||||
|
||||
if (framework is not null)
|
||||
return SemanticConfidence.Medium(reasoning.ToArray());
|
||||
|
||||
return SemanticConfidence.Low(reasoning.ToArray());
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateId(SemanticAnalysisContext context)
|
||||
{
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-node-{hash[..12]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Python semantic adapter for inferring intent and capabilities.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 8).
|
||||
/// Detects Django, Flask, FastAPI, Celery, Click, Typer, Lambda handlers.
|
||||
/// </remarks>
|
||||
public sealed class PythonSemanticAdapter : ISemanticEntrypointAnalyzer
|
||||
{
|
||||
public IReadOnlyList<string> SupportedLanguages => ["python"];
|
||||
public int Priority => 100;
|
||||
|
||||
private static readonly FrozenDictionary<string, ApplicationIntent> FrameworkIntentMap = new Dictionary<string, ApplicationIntent>
|
||||
{
|
||||
// Web frameworks
|
||||
["django"] = ApplicationIntent.WebServer,
|
||||
["flask"] = ApplicationIntent.WebServer,
|
||||
["fastapi"] = ApplicationIntent.WebServer,
|
||||
["starlette"] = ApplicationIntent.WebServer,
|
||||
["tornado"] = ApplicationIntent.WebServer,
|
||||
["aiohttp"] = ApplicationIntent.WebServer,
|
||||
["sanic"] = ApplicationIntent.WebServer,
|
||||
["bottle"] = ApplicationIntent.WebServer,
|
||||
["pyramid"] = ApplicationIntent.WebServer,
|
||||
["falcon"] = ApplicationIntent.WebServer,
|
||||
["quart"] = ApplicationIntent.WebServer,
|
||||
["litestar"] = ApplicationIntent.WebServer,
|
||||
|
||||
// Workers/queues
|
||||
["celery"] = ApplicationIntent.Worker,
|
||||
["rq"] = ApplicationIntent.Worker,
|
||||
["dramatiq"] = ApplicationIntent.Worker,
|
||||
["huey"] = ApplicationIntent.Worker,
|
||||
["arq"] = ApplicationIntent.Worker,
|
||||
|
||||
// CLI
|
||||
["click"] = ApplicationIntent.CliTool,
|
||||
["typer"] = ApplicationIntent.CliTool,
|
||||
["argparse"] = ApplicationIntent.CliTool,
|
||||
["fire"] = ApplicationIntent.CliTool,
|
||||
|
||||
// Serverless
|
||||
["awslambdaric"] = ApplicationIntent.Serverless,
|
||||
["aws_lambda_powertools"] = ApplicationIntent.Serverless,
|
||||
["mangum"] = ApplicationIntent.Serverless,
|
||||
["chalice"] = ApplicationIntent.Serverless,
|
||||
|
||||
// gRPC
|
||||
["grpcio"] = ApplicationIntent.RpcServer,
|
||||
["grpc"] = ApplicationIntent.RpcServer,
|
||||
|
||||
// GraphQL
|
||||
["graphene"] = ApplicationIntent.GraphQlServer,
|
||||
["strawberry"] = ApplicationIntent.GraphQlServer,
|
||||
["ariadne"] = ApplicationIntent.GraphQlServer,
|
||||
|
||||
// ML inference
|
||||
["tensorflow_serving"] = ApplicationIntent.MlInferenceServer,
|
||||
["mlflow"] = ApplicationIntent.MlInferenceServer,
|
||||
["bentoml"] = ApplicationIntent.MlInferenceServer,
|
||||
["ray"] = ApplicationIntent.MlInferenceServer,
|
||||
|
||||
// Stream processing
|
||||
["faust"] = ApplicationIntent.StreamProcessor,
|
||||
["kafka"] = ApplicationIntent.StreamProcessor,
|
||||
|
||||
// Schedulers
|
||||
["apscheduler"] = ApplicationIntent.ScheduledTask,
|
||||
["schedule"] = ApplicationIntent.ScheduledTask,
|
||||
|
||||
// Metrics/monitoring
|
||||
["prometheus_client"] = ApplicationIntent.MetricsCollector,
|
||||
|
||||
// Testing (should be deprioritized)
|
||||
["pytest"] = ApplicationIntent.TestRunner,
|
||||
["unittest"] = ApplicationIntent.TestRunner,
|
||||
["nose"] = ApplicationIntent.TestRunner,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static readonly FrozenDictionary<string, CapabilityClass> ImportCapabilityMap = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
// Network
|
||||
["socket"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["http.server"] = CapabilityClass.NetworkListen,
|
||||
["http.client"] = CapabilityClass.NetworkConnect,
|
||||
["urllib"] = CapabilityClass.NetworkConnect,
|
||||
["urllib3"] = CapabilityClass.NetworkConnect,
|
||||
["requests"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["httpx"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["aiohttp"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
|
||||
// File system
|
||||
["os"] = CapabilityClass.FileRead | CapabilityClass.FileWrite | CapabilityClass.EnvironmentRead,
|
||||
["os.path"] = CapabilityClass.FileRead,
|
||||
["pathlib"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["shutil"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["tempfile"] = CapabilityClass.FileWrite,
|
||||
["io"] = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
["glob"] = CapabilityClass.FileRead,
|
||||
|
||||
// Process
|
||||
["subprocess"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["multiprocessing"] = CapabilityClass.ProcessSpawn,
|
||||
["os.system"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["signal"] = CapabilityClass.ProcessSignal,
|
||||
|
||||
// Crypto
|
||||
["cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
["hashlib"] = CapabilityClass.CryptoEncrypt,
|
||||
["secrets"] = CapabilityClass.CryptoKeyGen,
|
||||
["ssl"] = CapabilityClass.CryptoEncrypt,
|
||||
["nacl"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
["pynacl"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
|
||||
// Databases
|
||||
["sqlite3"] = CapabilityClass.DatabaseSql,
|
||||
["psycopg2"] = CapabilityClass.DatabaseSql,
|
||||
["psycopg"] = CapabilityClass.DatabaseSql,
|
||||
["asyncpg"] = CapabilityClass.DatabaseSql,
|
||||
["pymysql"] = CapabilityClass.DatabaseSql,
|
||||
["mysql.connector"] = CapabilityClass.DatabaseSql,
|
||||
["sqlalchemy"] = CapabilityClass.DatabaseSql,
|
||||
["pymongo"] = CapabilityClass.DatabaseNoSql,
|
||||
["motor"] = CapabilityClass.DatabaseNoSql,
|
||||
["redis"] = CapabilityClass.CacheAccess,
|
||||
["aioredis"] = CapabilityClass.CacheAccess,
|
||||
["elasticsearch"] = CapabilityClass.DatabaseNoSql,
|
||||
|
||||
// Message queues
|
||||
["pika"] = CapabilityClass.MessageQueue,
|
||||
["kombu"] = CapabilityClass.MessageQueue,
|
||||
["aiokafka"] = CapabilityClass.MessageQueue,
|
||||
["confluent_kafka"] = CapabilityClass.MessageQueue,
|
||||
|
||||
// Cloud SDKs
|
||||
["boto3"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["botocore"] = CapabilityClass.CloudSdk,
|
||||
["google.cloud"] = CapabilityClass.CloudSdk,
|
||||
["azure"] = CapabilityClass.CloudSdk,
|
||||
|
||||
// Unsafe patterns
|
||||
["pickle"] = CapabilityClass.UnsafeDeserialization,
|
||||
["marshal"] = CapabilityClass.UnsafeDeserialization,
|
||||
["yaml"] = CapabilityClass.UnsafeDeserialization, // yaml.load without Loader
|
||||
["xml.etree"] = CapabilityClass.XmlExternalEntities,
|
||||
["lxml"] = CapabilityClass.XmlExternalEntities,
|
||||
["exec"] = CapabilityClass.DynamicCodeEval,
|
||||
["eval"] = CapabilityClass.DynamicCodeEval,
|
||||
["compile"] = CapabilityClass.DynamicCodeEval,
|
||||
|
||||
// Template engines
|
||||
["jinja2"] = CapabilityClass.TemplateRendering,
|
||||
["mako"] = CapabilityClass.TemplateRendering,
|
||||
["django.template"] = CapabilityClass.TemplateRendering,
|
||||
|
||||
// Logging/metrics
|
||||
["logging"] = CapabilityClass.LogEmit,
|
||||
["structlog"] = CapabilityClass.LogEmit,
|
||||
["prometheus_client"] = CapabilityClass.MetricsEmit,
|
||||
["opentelemetry"] = CapabilityClass.TracingEmit | CapabilityClass.MetricsEmit,
|
||||
|
||||
// Auth
|
||||
["passlib"] = CapabilityClass.Authentication,
|
||||
["python_jwt"] = CapabilityClass.Authentication | CapabilityClass.SessionManagement,
|
||||
["authlib"] = CapabilityClass.Authentication,
|
||||
|
||||
// Secrets
|
||||
["dotenv"] = CapabilityClass.SecretAccess | CapabilityClass.ConfigLoad,
|
||||
["hvac"] = CapabilityClass.SecretAccess,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public async Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var builder = new SemanticEntrypointBuilder()
|
||||
.WithId(GenerateId(context))
|
||||
.WithSpecification(context.Specification)
|
||||
.WithLanguage("python");
|
||||
|
||||
var reasoningChain = new List<string>();
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var framework = (string?)null;
|
||||
|
||||
// Analyze dependencies to determine intent and capabilities
|
||||
if (context.Dependencies.TryGetValue("python", out var deps))
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
var normalizedDep = NormalizeDependency(dep);
|
||||
|
||||
// Check framework intent
|
||||
if (FrameworkIntentMap.TryGetValue(normalizedDep, out var mappedIntent))
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown || IsHigherPriority(mappedIntent, intent))
|
||||
{
|
||||
intent = mappedIntent;
|
||||
framework = dep;
|
||||
reasoningChain.Add($"Detected {dep} -> {intent}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check capability imports
|
||||
if (ImportCapabilityMap.TryGetValue(normalizedDep, out var capability))
|
||||
{
|
||||
builder.AddCapability(capability);
|
||||
reasoningChain.Add($"Import {dep} -> {capability}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze entrypoint command for additional signals
|
||||
var cmdSignals = AnalyzeCommand(context.Specification);
|
||||
if (cmdSignals.Intent != ApplicationIntent.Unknown && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = cmdSignals.Intent;
|
||||
reasoningChain.Add($"Command pattern -> {intent}");
|
||||
}
|
||||
|
||||
foreach (var cap in GetCapabilityFlags(cmdSignals.Capabilities))
|
||||
{
|
||||
builder.AddCapability(cap);
|
||||
}
|
||||
|
||||
// Check exposed ports for web server inference
|
||||
if (context.Specification.ExposedPorts.Length > 0)
|
||||
{
|
||||
var webPorts = context.Specification.ExposedPorts.Where(IsWebPort).ToList();
|
||||
if (webPorts.Count > 0 && intent == ApplicationIntent.Unknown)
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
reasoningChain.Add($"Exposed web ports: {string.Join(", ", webPorts)}");
|
||||
}
|
||||
builder.AddCapability(CapabilityClass.NetworkListen);
|
||||
}
|
||||
|
||||
// Build confidence based on evidence
|
||||
var confidence = DetermineConfidence(reasoningChain, intent, framework);
|
||||
|
||||
builder.WithIntent(intent)
|
||||
.WithConfidence(confidence);
|
||||
|
||||
if (framework is not null)
|
||||
{
|
||||
builder.WithFramework(framework);
|
||||
}
|
||||
|
||||
return await Task.FromResult(builder.Build());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep)
|
||||
{
|
||||
return dep.ToLowerInvariant()
|
||||
.Replace("-", "_")
|
||||
.Split('[')[0]
|
||||
.Split('=')[0]
|
||||
.Split('>')[0]
|
||||
.Split('<')[0]
|
||||
.Trim();
|
||||
}
|
||||
|
||||
private static bool IsHigherPriority(ApplicationIntent newer, ApplicationIntent current)
|
||||
{
|
||||
// WebServer and Worker are higher priority than CLI/Batch
|
||||
var priorityOrder = new[]
|
||||
{
|
||||
ApplicationIntent.Unknown,
|
||||
ApplicationIntent.TestRunner,
|
||||
ApplicationIntent.CliTool,
|
||||
ApplicationIntent.BatchJob,
|
||||
ApplicationIntent.Worker,
|
||||
ApplicationIntent.Serverless,
|
||||
ApplicationIntent.WebServer,
|
||||
ApplicationIntent.RpcServer,
|
||||
ApplicationIntent.GraphQlServer,
|
||||
};
|
||||
|
||||
return Array.IndexOf(priorityOrder, newer) > Array.IndexOf(priorityOrder, current);
|
||||
}
|
||||
|
||||
private static (ApplicationIntent Intent, CapabilityClass Capabilities) AnalyzeCommand(EntrypointSpecification spec)
|
||||
{
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
var intent = ApplicationIntent.Unknown;
|
||||
var caps = CapabilityClass.None;
|
||||
|
||||
if (cmd.Contains("gunicorn") || cmd.Contains("uvicorn") || cmd.Contains("hypercorn") ||
|
||||
cmd.Contains("daphne") || cmd.Contains("waitress"))
|
||||
{
|
||||
intent = ApplicationIntent.WebServer;
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
}
|
||||
else if (cmd.Contains("celery") && cmd.Contains("worker"))
|
||||
{
|
||||
intent = ApplicationIntent.Worker;
|
||||
caps |= CapabilityClass.MessageQueue;
|
||||
}
|
||||
else if (cmd.Contains("celery") && cmd.Contains("beat"))
|
||||
{
|
||||
intent = ApplicationIntent.ScheduledTask;
|
||||
}
|
||||
else if (cmd.Contains("python") && cmd.Contains("-m"))
|
||||
{
|
||||
// Module execution - could be anything
|
||||
if (cmd.Contains("flask"))
|
||||
intent = ApplicationIntent.WebServer;
|
||||
else if (cmd.Contains("django"))
|
||||
intent = ApplicationIntent.WebServer;
|
||||
}
|
||||
else if (cmd.Contains("pytest") || cmd.Contains("-m pytest"))
|
||||
{
|
||||
intent = ApplicationIntent.TestRunner;
|
||||
}
|
||||
|
||||
return (intent, caps);
|
||||
}
|
||||
|
||||
private static bool IsWebPort(int port)
|
||||
{
|
||||
return port is 80 or 443 or 8000 or 8080 or 8443 or 3000 or 5000 or 5001 or 9000;
|
||||
}
|
||||
|
||||
private static SemanticConfidence DetermineConfidence(List<string> reasoning, ApplicationIntent intent, string? framework)
|
||||
{
|
||||
if (intent == ApplicationIntent.Unknown)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
if (framework is not null && reasoning.Count >= 3)
|
||||
return SemanticConfidence.High(reasoning.ToArray());
|
||||
|
||||
if (framework is not null)
|
||||
return SemanticConfidence.Medium(reasoning.ToArray());
|
||||
|
||||
return SemanticConfidence.Low(reasoning.ToArray());
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateId(SemanticAnalysisContext context)
|
||||
{
|
||||
var hash = context.ImageDigest ?? Guid.NewGuid().ToString("N");
|
||||
return $"sem-py-{hash[..12]}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Detects capabilities from imports, dependencies, and code patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 13).
|
||||
/// Analyzes dependencies to infer what capabilities an application has.
|
||||
/// </remarks>
|
||||
public sealed class CapabilityDetector
|
||||
{
|
||||
private readonly FrozenDictionary<string, CapabilityClass> _pythonCapabilities;
|
||||
private readonly FrozenDictionary<string, CapabilityClass> _nodeCapabilities;
|
||||
private readonly FrozenDictionary<string, CapabilityClass> _javaCapabilities;
|
||||
private readonly FrozenDictionary<string, CapabilityClass> _goCapabilities;
|
||||
private readonly FrozenDictionary<string, CapabilityClass> _dotnetCapabilities;
|
||||
|
||||
public CapabilityDetector()
|
||||
{
|
||||
_pythonCapabilities = BuildPythonCapabilities();
|
||||
_nodeCapabilities = BuildNodeCapabilities();
|
||||
_javaCapabilities = BuildJavaCapabilities();
|
||||
_goCapabilities = BuildGoCapabilities();
|
||||
_dotnetCapabilities = BuildDotNetCapabilities();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects capabilities from the analysis context.
|
||||
/// </summary>
|
||||
public CapabilityDetectionResult Detect(SemanticAnalysisContext context)
|
||||
{
|
||||
var capabilities = CapabilityClass.None;
|
||||
var evidence = new List<CapabilityEvidence>();
|
||||
|
||||
// Analyze by language
|
||||
foreach (var (lang, deps) in context.Dependencies)
|
||||
{
|
||||
var langCaps = DetectForLanguage(lang, deps);
|
||||
foreach (var (cap, ev) in langCaps)
|
||||
{
|
||||
capabilities |= cap;
|
||||
evidence.Add(ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze exposed ports
|
||||
var portCaps = DetectFromPorts(context.Specification.ExposedPorts);
|
||||
capabilities |= portCaps.Capabilities;
|
||||
evidence.AddRange(portCaps.Evidence);
|
||||
|
||||
// Analyze environment variables
|
||||
var envCaps = DetectFromEnvironment(context.Specification.Environment);
|
||||
capabilities |= envCaps.Capabilities;
|
||||
evidence.AddRange(envCaps.Evidence);
|
||||
|
||||
// Analyze volumes
|
||||
var volCaps = DetectFromVolumes(context.Specification.Volumes);
|
||||
capabilities |= volCaps.Capabilities;
|
||||
evidence.AddRange(volCaps.Evidence);
|
||||
|
||||
// Analyze command
|
||||
var cmdCaps = DetectFromCommand(context.Specification);
|
||||
capabilities |= cmdCaps.Capabilities;
|
||||
evidence.AddRange(cmdCaps.Evidence);
|
||||
|
||||
return new CapabilityDetectionResult
|
||||
{
|
||||
Capabilities = capabilities,
|
||||
Evidence = evidence.ToImmutableArray(),
|
||||
Confidence = CalculateConfidence(evidence)
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<(CapabilityClass Capability, CapabilityEvidence Evidence)> DetectForLanguage(
|
||||
string language, IReadOnlyList<string> dependencies)
|
||||
{
|
||||
var capMap = language.ToLowerInvariant() switch
|
||||
{
|
||||
"python" => _pythonCapabilities,
|
||||
"node" or "javascript" or "typescript" => _nodeCapabilities,
|
||||
"java" or "kotlin" or "scala" => _javaCapabilities,
|
||||
"go" or "golang" => _goCapabilities,
|
||||
"dotnet" or "csharp" or "fsharp" => _dotnetCapabilities,
|
||||
_ => FrozenDictionary<string, CapabilityClass>.Empty
|
||||
};
|
||||
|
||||
foreach (var dep in dependencies)
|
||||
{
|
||||
var normalized = NormalizeDependency(dep, language);
|
||||
if (capMap.TryGetValue(normalized, out var capability))
|
||||
{
|
||||
yield return (capability, new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Dependency,
|
||||
Language = language,
|
||||
Artifact = dep,
|
||||
Capability = capability,
|
||||
Confidence = 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (CapabilityClass Capabilities, ImmutableArray<CapabilityEvidence> Evidence) DetectFromPorts(
|
||||
ImmutableArray<int> ports)
|
||||
{
|
||||
var caps = CapabilityClass.None;
|
||||
var evidence = new List<CapabilityEvidence>();
|
||||
|
||||
if (ports.Length > 0)
|
||||
{
|
||||
caps |= CapabilityClass.NetworkListen;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.ExposedPort,
|
||||
Artifact = string.Join(", ", ports),
|
||||
Capability = CapabilityClass.NetworkListen,
|
||||
Confidence = 1.0
|
||||
});
|
||||
|
||||
// Check for specific service ports
|
||||
foreach (var port in ports)
|
||||
{
|
||||
var portCap = port switch
|
||||
{
|
||||
5432 => CapabilityClass.DatabaseSql, // PostgreSQL
|
||||
3306 => CapabilityClass.DatabaseSql, // MySQL
|
||||
27017 => CapabilityClass.DatabaseNoSql, // MongoDB
|
||||
6379 => CapabilityClass.CacheAccess, // Redis
|
||||
5672 or 15672 => CapabilityClass.MessageQueue, // RabbitMQ
|
||||
9092 => CapabilityClass.MessageQueue, // Kafka
|
||||
_ => CapabilityClass.None
|
||||
};
|
||||
|
||||
if (portCap != CapabilityClass.None)
|
||||
{
|
||||
caps |= portCap;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.ExposedPort,
|
||||
Artifact = $"Port {port}",
|
||||
Capability = portCap,
|
||||
Confidence = 0.8
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (caps, evidence.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static (CapabilityClass Capabilities, ImmutableArray<CapabilityEvidence> Evidence) DetectFromEnvironment(
|
||||
ImmutableDictionary<string, string>? env)
|
||||
{
|
||||
if (env is null)
|
||||
return (CapabilityClass.None, ImmutableArray<CapabilityEvidence>.Empty);
|
||||
|
||||
var caps = CapabilityClass.None;
|
||||
var evidence = new List<CapabilityEvidence>();
|
||||
|
||||
var sensitivePatterns = new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["DATABASE_URL"] = CapabilityClass.DatabaseSql,
|
||||
["POSTGRES_"] = CapabilityClass.DatabaseSql,
|
||||
["MYSQL_"] = CapabilityClass.DatabaseSql,
|
||||
["MONGODB_"] = CapabilityClass.DatabaseNoSql,
|
||||
["REDIS_"] = CapabilityClass.CacheAccess,
|
||||
["RABBITMQ_"] = CapabilityClass.MessageQueue,
|
||||
["KAFKA_"] = CapabilityClass.MessageQueue,
|
||||
["AWS_"] = CapabilityClass.CloudSdk,
|
||||
["AZURE_"] = CapabilityClass.CloudSdk,
|
||||
["GCP_"] = CapabilityClass.CloudSdk,
|
||||
["GOOGLE_"] = CapabilityClass.CloudSdk,
|
||||
["API_KEY"] = CapabilityClass.SecretAccess,
|
||||
["SECRET"] = CapabilityClass.SecretAccess,
|
||||
["PASSWORD"] = CapabilityClass.SecretAccess,
|
||||
["TOKEN"] = CapabilityClass.SecretAccess,
|
||||
["SMTP_"] = CapabilityClass.EmailSend,
|
||||
["MAIL_"] = CapabilityClass.EmailSend,
|
||||
};
|
||||
|
||||
foreach (var key in env.Keys)
|
||||
{
|
||||
foreach (var (pattern, cap) in sensitivePatterns)
|
||||
{
|
||||
if (key.Contains(pattern, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
caps |= cap;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.EnvironmentVariable,
|
||||
Artifact = key,
|
||||
Capability = cap,
|
||||
Confidence = 0.7
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (caps, evidence.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static (CapabilityClass Capabilities, ImmutableArray<CapabilityEvidence> Evidence) DetectFromVolumes(
|
||||
ImmutableArray<string> volumes)
|
||||
{
|
||||
var caps = CapabilityClass.None;
|
||||
var evidence = new List<CapabilityEvidence>();
|
||||
|
||||
foreach (var volume in volumes)
|
||||
{
|
||||
caps |= CapabilityClass.FileRead | CapabilityClass.FileWrite;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Volume,
|
||||
Artifact = volume,
|
||||
Capability = CapabilityClass.FileRead | CapabilityClass.FileWrite,
|
||||
Confidence = 1.0
|
||||
});
|
||||
|
||||
// Check for sensitive paths
|
||||
if (volume.Contains("/var/run/docker.sock"))
|
||||
{
|
||||
caps |= CapabilityClass.ContainerEscape;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Volume,
|
||||
Artifact = volume,
|
||||
Capability = CapabilityClass.ContainerEscape,
|
||||
Confidence = 1.0
|
||||
});
|
||||
}
|
||||
else if (volume.Contains("/etc") || volume.Contains("/proc") || volume.Contains("/sys"))
|
||||
{
|
||||
caps |= CapabilityClass.SystemPrivileged;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Volume,
|
||||
Artifact = volume,
|
||||
Capability = CapabilityClass.SystemPrivileged,
|
||||
Confidence = 0.9
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (caps, evidence.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static (CapabilityClass Capabilities, ImmutableArray<CapabilityEvidence> Evidence) DetectFromCommand(
|
||||
EntrypointSpecification spec)
|
||||
{
|
||||
var caps = CapabilityClass.None;
|
||||
var evidence = new List<CapabilityEvidence>();
|
||||
|
||||
var cmd = string.Join(" ", spec.Entrypoint.Concat(spec.Cmd));
|
||||
|
||||
if (cmd.Contains("sh ") || cmd.Contains("bash ") || cmd.Contains("/bin/sh") || cmd.Contains("/bin/bash"))
|
||||
{
|
||||
caps |= CapabilityClass.ShellExecution;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Command,
|
||||
Artifact = cmd,
|
||||
Capability = CapabilityClass.ShellExecution,
|
||||
Confidence = 0.9
|
||||
});
|
||||
}
|
||||
|
||||
if (cmd.Contains("sudo") || cmd.Contains("su -"))
|
||||
{
|
||||
caps |= CapabilityClass.SystemPrivileged;
|
||||
evidence.Add(new CapabilityEvidence
|
||||
{
|
||||
Source = EvidenceSource.Command,
|
||||
Artifact = cmd,
|
||||
Capability = CapabilityClass.SystemPrivileged,
|
||||
Confidence = 0.95
|
||||
});
|
||||
}
|
||||
|
||||
return (caps, evidence.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static string NormalizeDependency(string dep, string language)
|
||||
{
|
||||
return language.ToLowerInvariant() switch
|
||||
{
|
||||
"python" => dep.ToLowerInvariant().Replace("-", "_").Split('[')[0].Split('=')[0].Trim(),
|
||||
"node" or "javascript" or "typescript" => dep.ToLowerInvariant().Split('@')[0].Trim(),
|
||||
"java" => dep.Split(':').Length >= 2 ? dep.Split(':')[1].ToLowerInvariant() : dep.ToLowerInvariant(),
|
||||
"go" => dep.Split('@')[0].Trim(),
|
||||
"dotnet" => dep.Split('/')[0].Trim(),
|
||||
_ => dep.ToLowerInvariant().Trim()
|
||||
};
|
||||
}
|
||||
|
||||
private static SemanticConfidence CalculateConfidence(List<CapabilityEvidence> evidence)
|
||||
{
|
||||
if (evidence.Count == 0)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
var avgConfidence = evidence.Average(e => e.Confidence);
|
||||
var reasons = evidence.Select(e => $"{e.Source}: {e.Artifact} -> {e.Capability}").ToArray();
|
||||
|
||||
return SemanticConfidence.FromScore(avgConfidence, reasons.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static FrozenDictionary<string, CapabilityClass> BuildPythonCapabilities() =>
|
||||
new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["socket"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["requests"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["httpx"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["subprocess"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["psycopg2"] = CapabilityClass.DatabaseSql,
|
||||
["sqlalchemy"] = CapabilityClass.DatabaseSql,
|
||||
["pymongo"] = CapabilityClass.DatabaseNoSql,
|
||||
["redis"] = CapabilityClass.CacheAccess,
|
||||
["celery"] = CapabilityClass.MessageQueue,
|
||||
["boto3"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
["pickle"] = CapabilityClass.UnsafeDeserialization,
|
||||
["jinja2"] = CapabilityClass.TemplateRendering,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static FrozenDictionary<string, CapabilityClass> BuildNodeCapabilities() =>
|
||||
new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["axios"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["express"] = CapabilityClass.NetworkListen | CapabilityClass.UserInput,
|
||||
["child_process"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["pg"] = CapabilityClass.DatabaseSql,
|
||||
["mongoose"] = CapabilityClass.DatabaseNoSql,
|
||||
["redis"] = CapabilityClass.CacheAccess,
|
||||
["amqplib"] = CapabilityClass.MessageQueue,
|
||||
["aws_sdk"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["crypto"] = CapabilityClass.CryptoEncrypt,
|
||||
["vm"] = CapabilityClass.DynamicCodeEval,
|
||||
["ejs"] = CapabilityClass.TemplateRendering,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static FrozenDictionary<string, CapabilityClass> BuildJavaCapabilities() =>
|
||||
new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["okhttp"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["spring_boot_starter_web"] = CapabilityClass.NetworkListen | CapabilityClass.UserInput,
|
||||
["jdbc"] = CapabilityClass.DatabaseSql,
|
||||
["hibernate"] = CapabilityClass.DatabaseSql,
|
||||
["mongo_java_driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["jedis"] = CapabilityClass.CacheAccess,
|
||||
["kafka_clients"] = CapabilityClass.MessageQueue,
|
||||
["aws_sdk_java"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["bouncycastle"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
["jackson"] = CapabilityClass.UnsafeDeserialization,
|
||||
["thymeleaf"] = CapabilityClass.TemplateRendering,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static FrozenDictionary<string, CapabilityClass> BuildGoCapabilities() =>
|
||||
new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["net/http"] = CapabilityClass.NetworkConnect | CapabilityClass.NetworkListen,
|
||||
["os/exec"] = CapabilityClass.ProcessSpawn | CapabilityClass.ShellExecution,
|
||||
["database/sql"] = CapabilityClass.DatabaseSql,
|
||||
["go.mongodb.org/mongo_driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["github.com/go_redis/redis"] = CapabilityClass.CacheAccess,
|
||||
["github.com/shopify/sarama"] = CapabilityClass.MessageQueue,
|
||||
["github.com/aws/aws_sdk_go"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["crypto"] = CapabilityClass.CryptoEncrypt,
|
||||
["encoding/gob"] = CapabilityClass.UnsafeDeserialization,
|
||||
["html/template"] = CapabilityClass.TemplateRendering,
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static FrozenDictionary<string, CapabilityClass> BuildDotNetCapabilities() =>
|
||||
new Dictionary<string, CapabilityClass>
|
||||
{
|
||||
["system.net.http"] = CapabilityClass.NetworkConnect | CapabilityClass.ExternalHttpApi,
|
||||
["microsoft.aspnetcore"] = CapabilityClass.NetworkListen | CapabilityClass.UserInput,
|
||||
["system.diagnostics.process"] = CapabilityClass.ProcessSpawn,
|
||||
["microsoft.entityframeworkcore"] = CapabilityClass.DatabaseSql,
|
||||
["mongodb.driver"] = CapabilityClass.DatabaseNoSql,
|
||||
["stackexchange.redis"] = CapabilityClass.CacheAccess,
|
||||
["rabbitmq.client"] = CapabilityClass.MessageQueue,
|
||||
["awssdk.core"] = CapabilityClass.CloudSdk | CapabilityClass.ObjectStorage,
|
||||
["system.security.cryptography"] = CapabilityClass.CryptoEncrypt | CapabilityClass.CryptoSign,
|
||||
["newtonsoft.json"] = CapabilityClass.UnsafeDeserialization,
|
||||
["razorlight"] = CapabilityClass.TemplateRendering,
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of capability detection.
|
||||
/// </summary>
|
||||
public sealed record CapabilityDetectionResult
|
||||
{
|
||||
public required CapabilityClass Capabilities { get; init; }
|
||||
public required ImmutableArray<CapabilityEvidence> Evidence { get; init; }
|
||||
public required SemanticConfidence Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evidence for a detected capability.
|
||||
/// </summary>
|
||||
public sealed record CapabilityEvidence
|
||||
{
|
||||
public required EvidenceSource Source { get; init; }
|
||||
public string? Language { get; init; }
|
||||
public required string Artifact { get; init; }
|
||||
public required CapabilityClass Capability { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source of capability evidence.
|
||||
/// </summary>
|
||||
public enum EvidenceSource
|
||||
{
|
||||
Dependency,
|
||||
Import,
|
||||
ExposedPort,
|
||||
EnvironmentVariable,
|
||||
Volume,
|
||||
Command,
|
||||
Label,
|
||||
CodePattern,
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Maps data flow boundaries from entrypoint through framework handlers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 15).
|
||||
/// Traces data flow edges from entrypoint to I/O boundaries.
|
||||
/// </remarks>
|
||||
public sealed class DataBoundaryMapper
|
||||
{
|
||||
private readonly FrozenDictionary<ApplicationIntent, List<DataFlowBoundaryType>> _intentBoundaries;
|
||||
private readonly FrozenDictionary<CapabilityClass, List<DataFlowBoundaryType>> _capabilityBoundaries;
|
||||
|
||||
public DataBoundaryMapper()
|
||||
{
|
||||
_intentBoundaries = BuildIntentBoundaries();
|
||||
_capabilityBoundaries = BuildCapabilityBoundaries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps data flow boundaries for the given context.
|
||||
/// </summary>
|
||||
public DataBoundaryMappingResult Map(
|
||||
SemanticAnalysisContext context,
|
||||
ApplicationIntent intent,
|
||||
CapabilityClass capabilities,
|
||||
IReadOnlyList<CapabilityEvidence> evidence)
|
||||
{
|
||||
var boundaries = new List<DataFlowBoundary>();
|
||||
|
||||
// Add boundaries based on intent
|
||||
if (_intentBoundaries.TryGetValue(intent, out var intentBoundaryTypes))
|
||||
{
|
||||
foreach (var boundaryType in intentBoundaryTypes)
|
||||
{
|
||||
boundaries.Add(CreateBoundary(boundaryType, $"Intent: {intent}", 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
// Add boundaries based on capabilities
|
||||
foreach (var cap in GetCapabilityFlags(capabilities))
|
||||
{
|
||||
if (_capabilityBoundaries.TryGetValue(cap, out var capBoundaryTypes))
|
||||
{
|
||||
foreach (var boundaryType in capBoundaryTypes)
|
||||
{
|
||||
if (!boundaries.Any(b => b.Type == boundaryType))
|
||||
{
|
||||
boundaries.Add(CreateBoundary(boundaryType, $"Capability: {cap}", 0.7));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add boundaries based on exposed ports
|
||||
foreach (var port in context.Specification.ExposedPorts)
|
||||
{
|
||||
var portBoundaries = InferFromPort(port);
|
||||
foreach (var boundary in portBoundaries)
|
||||
{
|
||||
if (!boundaries.Any(b => b.Type == boundary.Type))
|
||||
{
|
||||
boundaries.Add(boundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add boundaries based on environment variables
|
||||
if (context.Specification.Environment is not null)
|
||||
{
|
||||
var envBoundaries = InferFromEnvironment(context.Specification.Environment);
|
||||
foreach (var boundary in envBoundaries)
|
||||
{
|
||||
if (!boundaries.Any(b => b.Type == boundary.Type))
|
||||
{
|
||||
boundaries.Add(boundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add boundaries based on evidence
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
var evBoundaries = InferFromEvidence(ev);
|
||||
foreach (var boundary in evBoundaries)
|
||||
{
|
||||
if (!boundaries.Any(b => b.Type == boundary.Type))
|
||||
{
|
||||
boundaries.Add(boundary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Infer sensitivity for each boundary
|
||||
boundaries = boundaries.Select(b => InferSensitivity(b, capabilities)).ToList();
|
||||
|
||||
// Sort by security relevance
|
||||
boundaries = boundaries.OrderByDescending(b => b.Type.IsSecuritySensitive())
|
||||
.ThenByDescending(b => b.Confidence)
|
||||
.ToList();
|
||||
|
||||
return new DataBoundaryMappingResult
|
||||
{
|
||||
Boundaries = boundaries.ToImmutableArray(),
|
||||
InboundCount = boundaries.Count(b => b.Direction == DataFlowDirection.Inbound),
|
||||
OutboundCount = boundaries.Count(b => b.Direction == DataFlowDirection.Outbound),
|
||||
SecuritySensitiveCount = boundaries.Count(b => b.Type.IsSecuritySensitive()),
|
||||
Confidence = CalculateConfidence(boundaries)
|
||||
};
|
||||
}
|
||||
|
||||
private static DataFlowBoundary CreateBoundary(
|
||||
DataFlowBoundaryType type,
|
||||
string evidenceReason,
|
||||
double confidence)
|
||||
{
|
||||
return new DataFlowBoundary
|
||||
{
|
||||
Type = type,
|
||||
Direction = type.GetDefaultDirection(),
|
||||
Sensitivity = DataSensitivity.Unknown,
|
||||
Confidence = confidence,
|
||||
Evidence = ImmutableArray.Create(evidenceReason)
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<DataFlowBoundary> InferFromPort(int port)
|
||||
{
|
||||
var boundaries = new List<DataFlowBoundary>();
|
||||
|
||||
DataFlowBoundaryType? boundaryType = port switch
|
||||
{
|
||||
80 or 443 or 8080 or 8443 or 3000 or 5000 or 9000 => DataFlowBoundaryType.HttpRequest,
|
||||
5432 or 3306 or 1433 or 1521 => DataFlowBoundaryType.DatabaseQuery,
|
||||
6379 => DataFlowBoundaryType.CacheRead,
|
||||
5672 or 9092 => DataFlowBoundaryType.MessageReceive,
|
||||
25 or 587 or 465 => null, // SMTP - no direct boundary type
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (boundaryType.HasValue)
|
||||
{
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = boundaryType.Value,
|
||||
Direction = boundaryType.Value.GetDefaultDirection(),
|
||||
Sensitivity = DataSensitivity.Unknown,
|
||||
Confidence = 0.85,
|
||||
Evidence = ImmutableArray.Create($"Exposed port: {port}")
|
||||
});
|
||||
|
||||
// Add corresponding response boundary for request types
|
||||
if (boundaryType.Value == DataFlowBoundaryType.HttpRequest)
|
||||
{
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = DataFlowBoundaryType.HttpResponse,
|
||||
Direction = DataFlowDirection.Outbound,
|
||||
Sensitivity = DataSensitivity.Unknown,
|
||||
Confidence = 0.85,
|
||||
Evidence = ImmutableArray.Create($"HTTP port: {port}")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
private static IEnumerable<DataFlowBoundary> InferFromEnvironment(
|
||||
ImmutableDictionary<string, string> env)
|
||||
{
|
||||
var boundaries = new List<DataFlowBoundary>();
|
||||
|
||||
// Always add environment variable boundary if env vars are present
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = DataFlowBoundaryType.EnvironmentVar,
|
||||
Direction = DataFlowDirection.Inbound,
|
||||
Sensitivity = env.Keys.Any(k =>
|
||||
k.Contains("SECRET", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("PASSWORD", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("KEY", StringComparison.OrdinalIgnoreCase) ||
|
||||
k.Contains("TOKEN", StringComparison.OrdinalIgnoreCase))
|
||||
? DataSensitivity.Restricted
|
||||
: DataSensitivity.Internal,
|
||||
Confidence = 1.0,
|
||||
Evidence = ImmutableArray.Create($"Environment variables: {env.Count}")
|
||||
});
|
||||
|
||||
// Check for specific service connections
|
||||
if (env.Keys.Any(k => k.Contains("DATABASE") || k.Contains("DB_")))
|
||||
{
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = DataFlowBoundaryType.DatabaseQuery,
|
||||
Direction = DataFlowDirection.Outbound,
|
||||
Sensitivity = DataSensitivity.Confidential,
|
||||
Confidence = 0.8,
|
||||
Evidence = ImmutableArray.Create("Database connection in environment")
|
||||
});
|
||||
}
|
||||
|
||||
if (env.Keys.Any(k => k.Contains("REDIS") || k.Contains("CACHE")))
|
||||
{
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = DataFlowBoundaryType.CacheRead,
|
||||
Direction = DataFlowDirection.Inbound,
|
||||
Sensitivity = DataSensitivity.Internal,
|
||||
Confidence = 0.8,
|
||||
Evidence = ImmutableArray.Create("Cache connection in environment")
|
||||
});
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
private static IEnumerable<DataFlowBoundary> InferFromEvidence(CapabilityEvidence evidence)
|
||||
{
|
||||
var boundaries = new List<DataFlowBoundary>();
|
||||
|
||||
// Map capability evidence to boundaries
|
||||
var boundaryType = evidence.Capability switch
|
||||
{
|
||||
CapabilityClass.DatabaseSql => DataFlowBoundaryType.DatabaseQuery,
|
||||
CapabilityClass.DatabaseNoSql => DataFlowBoundaryType.DatabaseQuery,
|
||||
CapabilityClass.CacheAccess => DataFlowBoundaryType.CacheRead,
|
||||
CapabilityClass.MessageQueue => DataFlowBoundaryType.MessageReceive,
|
||||
CapabilityClass.FileRead => DataFlowBoundaryType.FileInput,
|
||||
CapabilityClass.FileWrite => DataFlowBoundaryType.FileOutput,
|
||||
CapabilityClass.FileUpload => DataFlowBoundaryType.FileInput,
|
||||
CapabilityClass.ExternalHttpApi => DataFlowBoundaryType.ExternalApiCall,
|
||||
CapabilityClass.NetworkListen => DataFlowBoundaryType.SocketRead,
|
||||
CapabilityClass.NetworkConnect => DataFlowBoundaryType.SocketWrite,
|
||||
CapabilityClass.ProcessSpawn => DataFlowBoundaryType.ProcessSpawn,
|
||||
CapabilityClass.ConfigLoad => DataFlowBoundaryType.ConfigRead,
|
||||
CapabilityClass.EnvironmentRead => DataFlowBoundaryType.EnvironmentVar,
|
||||
_ => (DataFlowBoundaryType?)null
|
||||
};
|
||||
|
||||
if (boundaryType.HasValue)
|
||||
{
|
||||
boundaries.Add(new DataFlowBoundary
|
||||
{
|
||||
Type = boundaryType.Value,
|
||||
Direction = boundaryType.Value.GetDefaultDirection(),
|
||||
Sensitivity = DataSensitivity.Unknown,
|
||||
Confidence = evidence.Confidence * 0.9,
|
||||
Evidence = ImmutableArray.Create($"{evidence.Source}: {evidence.Artifact}")
|
||||
});
|
||||
}
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
private static DataFlowBoundary InferSensitivity(DataFlowBoundary boundary, CapabilityClass capabilities)
|
||||
{
|
||||
var sensitivity = boundary.Type switch
|
||||
{
|
||||
// Database operations are typically confidential
|
||||
DataFlowBoundaryType.DatabaseQuery or DataFlowBoundaryType.DatabaseResult =>
|
||||
capabilities.HasFlag(CapabilityClass.SecretAccess) ? DataSensitivity.Restricted : DataSensitivity.Confidential,
|
||||
|
||||
// Configuration and environment are internal/restricted
|
||||
DataFlowBoundaryType.ConfigRead or DataFlowBoundaryType.EnvironmentVar =>
|
||||
capabilities.HasFlag(CapabilityClass.SecretAccess) ? DataSensitivity.Restricted : DataSensitivity.Internal,
|
||||
|
||||
// HTTP requests can carry sensitive data
|
||||
DataFlowBoundaryType.HttpRequest =>
|
||||
capabilities.HasFlag(CapabilityClass.Authentication) ? DataSensitivity.Confidential : DataSensitivity.Internal,
|
||||
|
||||
// Process spawning is sensitive
|
||||
DataFlowBoundaryType.ProcessSpawn => DataSensitivity.Confidential,
|
||||
|
||||
// External API calls may expose internal data
|
||||
DataFlowBoundaryType.ExternalApiCall or DataFlowBoundaryType.ExternalApiResponse =>
|
||||
DataSensitivity.Internal,
|
||||
|
||||
// Cache and message queue are typically internal
|
||||
DataFlowBoundaryType.CacheRead or DataFlowBoundaryType.CacheWrite or
|
||||
DataFlowBoundaryType.MessageReceive or DataFlowBoundaryType.MessageSend =>
|
||||
DataSensitivity.Internal,
|
||||
|
||||
// Standard I/O is typically public
|
||||
DataFlowBoundaryType.StandardInput or DataFlowBoundaryType.StandardOutput or DataFlowBoundaryType.StandardError =>
|
||||
DataSensitivity.Public,
|
||||
|
||||
// Default to unknown
|
||||
_ => boundary.Sensitivity
|
||||
};
|
||||
|
||||
return boundary with { Sensitivity = sensitivity };
|
||||
}
|
||||
|
||||
private static IEnumerable<CapabilityClass> GetCapabilityFlags(CapabilityClass caps)
|
||||
{
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && caps.HasFlag(flag))
|
||||
yield return flag;
|
||||
}
|
||||
}
|
||||
|
||||
private static SemanticConfidence CalculateConfidence(List<DataFlowBoundary> boundaries)
|
||||
{
|
||||
if (boundaries.Count == 0)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
var avgConfidence = boundaries.Average(b => b.Confidence);
|
||||
var reasons = boundaries.Select(b => $"{b.Type} ({b.Direction})").ToArray();
|
||||
|
||||
return SemanticConfidence.FromScore(avgConfidence, reasons.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static FrozenDictionary<ApplicationIntent, List<DataFlowBoundaryType>> BuildIntentBoundaries() =>
|
||||
new Dictionary<ApplicationIntent, List<DataFlowBoundaryType>>
|
||||
{
|
||||
[ApplicationIntent.WebServer] =
|
||||
[
|
||||
DataFlowBoundaryType.HttpRequest,
|
||||
DataFlowBoundaryType.HttpResponse
|
||||
],
|
||||
[ApplicationIntent.CliTool] =
|
||||
[
|
||||
DataFlowBoundaryType.CommandLineArg,
|
||||
DataFlowBoundaryType.StandardInput,
|
||||
DataFlowBoundaryType.StandardOutput,
|
||||
DataFlowBoundaryType.StandardError
|
||||
],
|
||||
[ApplicationIntent.Worker] =
|
||||
[
|
||||
DataFlowBoundaryType.MessageReceive,
|
||||
DataFlowBoundaryType.MessageSend
|
||||
],
|
||||
[ApplicationIntent.BatchJob] =
|
||||
[
|
||||
DataFlowBoundaryType.FileInput,
|
||||
DataFlowBoundaryType.FileOutput,
|
||||
DataFlowBoundaryType.DatabaseQuery,
|
||||
DataFlowBoundaryType.DatabaseResult
|
||||
],
|
||||
[ApplicationIntent.Serverless] =
|
||||
[
|
||||
DataFlowBoundaryType.HttpRequest,
|
||||
DataFlowBoundaryType.HttpResponse,
|
||||
DataFlowBoundaryType.EnvironmentVar
|
||||
],
|
||||
[ApplicationIntent.DatabaseServer] =
|
||||
[
|
||||
DataFlowBoundaryType.SocketRead,
|
||||
DataFlowBoundaryType.SocketWrite,
|
||||
DataFlowBoundaryType.FileInput,
|
||||
DataFlowBoundaryType.FileOutput
|
||||
],
|
||||
[ApplicationIntent.MessageBroker] =
|
||||
[
|
||||
DataFlowBoundaryType.SocketRead,
|
||||
DataFlowBoundaryType.SocketWrite,
|
||||
DataFlowBoundaryType.MessageReceive,
|
||||
DataFlowBoundaryType.MessageSend
|
||||
],
|
||||
[ApplicationIntent.CacheServer] =
|
||||
[
|
||||
DataFlowBoundaryType.SocketRead,
|
||||
DataFlowBoundaryType.SocketWrite,
|
||||
DataFlowBoundaryType.CacheRead,
|
||||
DataFlowBoundaryType.CacheWrite
|
||||
],
|
||||
[ApplicationIntent.RpcServer] =
|
||||
[
|
||||
DataFlowBoundaryType.SocketRead,
|
||||
DataFlowBoundaryType.SocketWrite
|
||||
],
|
||||
[ApplicationIntent.GraphQlServer] =
|
||||
[
|
||||
DataFlowBoundaryType.HttpRequest,
|
||||
DataFlowBoundaryType.HttpResponse,
|
||||
DataFlowBoundaryType.DatabaseQuery
|
||||
],
|
||||
[ApplicationIntent.StreamProcessor] =
|
||||
[
|
||||
DataFlowBoundaryType.MessageReceive,
|
||||
DataFlowBoundaryType.MessageSend,
|
||||
DataFlowBoundaryType.DatabaseQuery
|
||||
],
|
||||
[ApplicationIntent.ProxyGateway] =
|
||||
[
|
||||
DataFlowBoundaryType.HttpRequest,
|
||||
DataFlowBoundaryType.HttpResponse,
|
||||
DataFlowBoundaryType.ExternalApiCall,
|
||||
DataFlowBoundaryType.ExternalApiResponse
|
||||
],
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private static FrozenDictionary<CapabilityClass, List<DataFlowBoundaryType>> BuildCapabilityBoundaries() =>
|
||||
new Dictionary<CapabilityClass, List<DataFlowBoundaryType>>
|
||||
{
|
||||
[CapabilityClass.DatabaseSql] = [DataFlowBoundaryType.DatabaseQuery, DataFlowBoundaryType.DatabaseResult],
|
||||
[CapabilityClass.DatabaseNoSql] = [DataFlowBoundaryType.DatabaseQuery, DataFlowBoundaryType.DatabaseResult],
|
||||
[CapabilityClass.CacheAccess] = [DataFlowBoundaryType.CacheRead, DataFlowBoundaryType.CacheWrite],
|
||||
[CapabilityClass.MessageQueue] = [DataFlowBoundaryType.MessageReceive, DataFlowBoundaryType.MessageSend],
|
||||
[CapabilityClass.FileRead] = [DataFlowBoundaryType.FileInput],
|
||||
[CapabilityClass.FileWrite] = [DataFlowBoundaryType.FileOutput],
|
||||
[CapabilityClass.FileUpload] = [DataFlowBoundaryType.FileInput],
|
||||
[CapabilityClass.ExternalHttpApi] = [DataFlowBoundaryType.ExternalApiCall, DataFlowBoundaryType.ExternalApiResponse],
|
||||
[CapabilityClass.ProcessSpawn] = [DataFlowBoundaryType.ProcessSpawn],
|
||||
[CapabilityClass.ConfigLoad] = [DataFlowBoundaryType.ConfigRead],
|
||||
[CapabilityClass.EnvironmentRead] = [DataFlowBoundaryType.EnvironmentVar],
|
||||
[CapabilityClass.NetworkListen] = [DataFlowBoundaryType.SocketRead, DataFlowBoundaryType.SocketWrite],
|
||||
[CapabilityClass.NetworkConnect] = [DataFlowBoundaryType.SocketWrite],
|
||||
[CapabilityClass.UserInput] = [DataFlowBoundaryType.HttpRequest],
|
||||
}.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of data boundary mapping.
|
||||
/// </summary>
|
||||
public sealed record DataBoundaryMappingResult
|
||||
{
|
||||
public required ImmutableArray<DataFlowBoundary> Boundaries { get; init; }
|
||||
public required int InboundCount { get; init; }
|
||||
public required int OutboundCount { get; init; }
|
||||
public required int SecuritySensitiveCount { get; init; }
|
||||
public required SemanticConfidence Confidence { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Infers threat vectors from capabilities and framework patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 14).
|
||||
/// Maps capabilities to potential attack vectors with confidence scoring.
|
||||
/// </remarks>
|
||||
public sealed class ThreatVectorInferrer
|
||||
{
|
||||
private readonly FrozenDictionary<ThreatVectorType, ThreatVectorRule> _rules;
|
||||
|
||||
public ThreatVectorInferrer()
|
||||
{
|
||||
_rules = BuildRules();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infers threat vectors from detected capabilities and intent.
|
||||
/// </summary>
|
||||
public ThreatInferenceResult Infer(
|
||||
CapabilityClass capabilities,
|
||||
ApplicationIntent intent,
|
||||
IReadOnlyList<CapabilityEvidence> evidence)
|
||||
{
|
||||
var threats = new List<ThreatVector>();
|
||||
|
||||
foreach (var (threatType, rule) in _rules)
|
||||
{
|
||||
var matchResult = EvaluateRule(rule, capabilities, intent, evidence);
|
||||
if (matchResult.Matches)
|
||||
{
|
||||
threats.Add(new ThreatVector
|
||||
{
|
||||
Type = threatType,
|
||||
Confidence = matchResult.Confidence,
|
||||
ContributingCapabilities = matchResult.MatchedCapabilities,
|
||||
Evidence = matchResult.Evidence,
|
||||
EntryPaths = ImmutableArray<string>.Empty,
|
||||
Metadata = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by confidence descending
|
||||
threats = threats.OrderByDescending(t => t.Confidence).ToList();
|
||||
|
||||
return new ThreatInferenceResult
|
||||
{
|
||||
ThreatVectors = threats.ToImmutableArray(),
|
||||
OverallRiskScore = CalculateRiskScore(threats),
|
||||
Confidence = CalculateConfidence(threats)
|
||||
};
|
||||
}
|
||||
|
||||
private static RuleMatchResult EvaluateRule(
|
||||
ThreatVectorRule rule,
|
||||
CapabilityClass capabilities,
|
||||
ApplicationIntent intent,
|
||||
IReadOnlyList<CapabilityEvidence> evidence)
|
||||
{
|
||||
var matchedCaps = CapabilityClass.None;
|
||||
var evidenceStrings = new List<string>();
|
||||
var score = 0.0;
|
||||
|
||||
// Check required capabilities
|
||||
foreach (var reqCap in rule.RequiredCapabilities)
|
||||
{
|
||||
if (capabilities.HasFlag(reqCap))
|
||||
{
|
||||
matchedCaps |= reqCap;
|
||||
score += 0.3;
|
||||
evidenceStrings.Add($"Has capability: {reqCap}");
|
||||
}
|
||||
}
|
||||
|
||||
// Must have all required capabilities
|
||||
var hasAllRequired = rule.RequiredCapabilities.All(c => capabilities.HasFlag(c));
|
||||
if (!hasAllRequired && rule.RequiredCapabilities.Count > 0)
|
||||
{
|
||||
return RuleMatchResult.NoMatch;
|
||||
}
|
||||
|
||||
// Check optional capabilities (boost confidence)
|
||||
foreach (var optCap in rule.OptionalCapabilities)
|
||||
{
|
||||
if (capabilities.HasFlag(optCap))
|
||||
{
|
||||
matchedCaps |= optCap;
|
||||
score += 0.1;
|
||||
evidenceStrings.Add($"Has optional capability: {optCap}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check intent match
|
||||
if (rule.RequiredIntents.Contains(intent))
|
||||
{
|
||||
score += 0.2;
|
||||
evidenceStrings.Add($"Intent matches: {intent}");
|
||||
}
|
||||
else if (rule.RequiredIntents.Count > 0 && !rule.RequiredIntents.Contains(ApplicationIntent.Unknown))
|
||||
{
|
||||
// Intent mismatch reduces confidence but doesn't eliminate
|
||||
score *= 0.7;
|
||||
}
|
||||
|
||||
// Check for specific evidence patterns
|
||||
foreach (var ev in evidence)
|
||||
{
|
||||
if (rule.EvidencePatterns.Any(p => ev.Artifact.Contains(p, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
score += 0.15;
|
||||
evidenceStrings.Add($"Evidence pattern: {ev.Artifact}");
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and apply base weight
|
||||
var finalScore = Math.Min(1.0, score * rule.BaseWeight);
|
||||
|
||||
return new RuleMatchResult
|
||||
{
|
||||
Matches = finalScore >= 0.3,
|
||||
Confidence = finalScore,
|
||||
MatchedCapabilities = matchedCaps,
|
||||
Evidence = evidenceStrings.ToImmutableArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static double CalculateRiskScore(List<ThreatVector> threats)
|
||||
{
|
||||
if (threats.Count == 0)
|
||||
return 0.0;
|
||||
|
||||
// Weighted sum with diminishing returns for multiple threats
|
||||
var score = 0.0;
|
||||
for (var i = 0; i < threats.Count; i++)
|
||||
{
|
||||
var weight = 1.0 / (i + 1); // Diminishing returns
|
||||
score += threats[i].Confidence * weight * GetSeverityWeight(threats[i].Type);
|
||||
}
|
||||
|
||||
return Math.Min(1.0, score);
|
||||
}
|
||||
|
||||
private static double GetSeverityWeight(ThreatVectorType type) => type switch
|
||||
{
|
||||
ThreatVectorType.Rce => 1.0,
|
||||
ThreatVectorType.ContainerEscape => 1.0,
|
||||
ThreatVectorType.PrivilegeEscalation => 0.95,
|
||||
ThreatVectorType.SqlInjection => 0.9,
|
||||
ThreatVectorType.CommandInjection => 0.9,
|
||||
ThreatVectorType.InsecureDeserialization => 0.85,
|
||||
ThreatVectorType.PathTraversal => 0.8,
|
||||
ThreatVectorType.Ssrf => 0.8,
|
||||
ThreatVectorType.AuthenticationBypass => 0.85,
|
||||
ThreatVectorType.AuthorizationBypass => 0.8,
|
||||
ThreatVectorType.XxeInjection => 0.75,
|
||||
ThreatVectorType.TemplateInjection => 0.75,
|
||||
ThreatVectorType.Xss => 0.7,
|
||||
ThreatVectorType.LdapInjection => 0.7,
|
||||
ThreatVectorType.Idor => 0.65,
|
||||
ThreatVectorType.Csrf => 0.6,
|
||||
ThreatVectorType.OpenRedirect => 0.5,
|
||||
ThreatVectorType.InformationDisclosure => 0.5,
|
||||
ThreatVectorType.LogInjection => 0.4,
|
||||
ThreatVectorType.HeaderInjection => 0.4,
|
||||
ThreatVectorType.DenialOfService => 0.3,
|
||||
ThreatVectorType.ReDoS => 0.3,
|
||||
ThreatVectorType.MassAssignment => 0.5,
|
||||
ThreatVectorType.CryptoWeakness => 0.5,
|
||||
_ => 0.5
|
||||
};
|
||||
|
||||
private static SemanticConfidence CalculateConfidence(List<ThreatVector> threats)
|
||||
{
|
||||
if (threats.Count == 0)
|
||||
return SemanticConfidence.Unknown();
|
||||
|
||||
var avgConfidence = threats.Average(t => t.Confidence);
|
||||
var reasons = threats.Select(t => $"{t.Type}: {t.Confidence:P0}").ToArray();
|
||||
|
||||
return SemanticConfidence.FromScore(avgConfidence, reasons.ToImmutableArray());
|
||||
}
|
||||
|
||||
private static FrozenDictionary<ThreatVectorType, ThreatVectorRule> BuildRules() =>
|
||||
new Dictionary<ThreatVectorType, ThreatVectorRule>
|
||||
{
|
||||
[ThreatVectorType.SqlInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.DatabaseSql, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.NetworkListen],
|
||||
RequiredIntents = [ApplicationIntent.WebServer, ApplicationIntent.RpcServer, ApplicationIntent.GraphQlServer],
|
||||
EvidencePatterns = ["sql", "query", "database", "orm"],
|
||||
BaseWeight = 1.0
|
||||
},
|
||||
[ThreatVectorType.Ssrf] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.NetworkConnect, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.ExternalHttpApi, CapabilityClass.CloudSdk],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["http", "url", "request", "fetch"],
|
||||
BaseWeight = 0.9
|
||||
},
|
||||
[ThreatVectorType.Rce] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.ShellExecution],
|
||||
OptionalCapabilities = [CapabilityClass.ProcessSpawn, CapabilityClass.DynamicCodeEval, CapabilityClass.UserInput],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["exec", "spawn", "system", "shell", "eval"],
|
||||
BaseWeight = 1.0
|
||||
},
|
||||
[ThreatVectorType.CommandInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.ProcessSpawn, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.ShellExecution],
|
||||
RequiredIntents = [ApplicationIntent.WebServer, ApplicationIntent.CliTool],
|
||||
EvidencePatterns = ["command", "exec", "run", "subprocess"],
|
||||
BaseWeight = 1.0
|
||||
},
|
||||
[ThreatVectorType.PathTraversal] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.FileRead, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.FileWrite, CapabilityClass.FileUpload],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["path", "file", "download", "upload"],
|
||||
BaseWeight = 0.85
|
||||
},
|
||||
[ThreatVectorType.Xss] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.TemplateRendering, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.NetworkListen],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["template", "html", "render", "view"],
|
||||
BaseWeight = 0.8
|
||||
},
|
||||
[ThreatVectorType.InsecureDeserialization] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UnsafeDeserialization],
|
||||
OptionalCapabilities = [CapabilityClass.UserInput, CapabilityClass.MessageQueue],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["pickle", "serialize", "unmarshal", "deserialize", "jackson"],
|
||||
BaseWeight = 0.95
|
||||
},
|
||||
[ThreatVectorType.TemplateInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.TemplateRendering, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.DynamicCodeEval],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["jinja", "template", "render", "ssti"],
|
||||
BaseWeight = 0.9
|
||||
},
|
||||
[ThreatVectorType.XxeInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.XmlExternalEntities],
|
||||
OptionalCapabilities = [CapabilityClass.UserInput, CapabilityClass.FileRead],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["xml", "parse", "dom", "sax"],
|
||||
BaseWeight = 0.85
|
||||
},
|
||||
[ThreatVectorType.AuthenticationBypass] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.Authentication],
|
||||
OptionalCapabilities = [CapabilityClass.SessionManagement, CapabilityClass.UserInput],
|
||||
RequiredIntents = [ApplicationIntent.WebServer, ApplicationIntent.RpcServer],
|
||||
EvidencePatterns = ["auth", "login", "jwt", "session", "token"],
|
||||
BaseWeight = 0.7
|
||||
},
|
||||
[ThreatVectorType.AuthorizationBypass] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.Authorization],
|
||||
OptionalCapabilities = [CapabilityClass.UserInput],
|
||||
RequiredIntents = [ApplicationIntent.WebServer, ApplicationIntent.RpcServer],
|
||||
EvidencePatterns = ["rbac", "permission", "role", "access"],
|
||||
BaseWeight = 0.7
|
||||
},
|
||||
[ThreatVectorType.ContainerEscape] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.ContainerEscape],
|
||||
OptionalCapabilities = [CapabilityClass.SystemPrivileged, CapabilityClass.KernelModule],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["docker.sock", "privileged", "hostpid", "hostnetwork"],
|
||||
BaseWeight = 1.0
|
||||
},
|
||||
[ThreatVectorType.PrivilegeEscalation] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.SystemPrivileged],
|
||||
OptionalCapabilities = [CapabilityClass.ProcessSpawn, CapabilityClass.FileWrite],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["sudo", "setuid", "capabilities", "root"],
|
||||
BaseWeight = 0.9
|
||||
},
|
||||
[ThreatVectorType.LdapInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.NetworkConnect, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.Authentication],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["ldap", "ldap3", "directory"],
|
||||
BaseWeight = 0.8
|
||||
},
|
||||
[ThreatVectorType.Csrf] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.SessionManagement, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [CapabilityClass.NetworkListen],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["form", "post", "session", "cookie"],
|
||||
BaseWeight = 0.6
|
||||
},
|
||||
[ThreatVectorType.OpenRedirect] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UserInput, CapabilityClass.NetworkListen],
|
||||
OptionalCapabilities = [],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["redirect", "url", "return", "next"],
|
||||
BaseWeight = 0.5
|
||||
},
|
||||
[ThreatVectorType.InformationDisclosure] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.LogEmit],
|
||||
OptionalCapabilities = [CapabilityClass.SecretAccess, CapabilityClass.ConfigLoad],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["log", "debug", "error", "stack"],
|
||||
BaseWeight = 0.4
|
||||
},
|
||||
[ThreatVectorType.DenialOfService] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.NetworkListen],
|
||||
OptionalCapabilities = [CapabilityClass.UserInput],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["rate", "limit", "timeout"],
|
||||
BaseWeight = 0.3
|
||||
},
|
||||
[ThreatVectorType.ReDoS] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["regex", "pattern", "match", "replace"],
|
||||
BaseWeight = 0.4
|
||||
},
|
||||
[ThreatVectorType.MassAssignment] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UserInput, CapabilityClass.DatabaseSql],
|
||||
OptionalCapabilities = [],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["model", "bind", "update", "create"],
|
||||
BaseWeight = 0.6
|
||||
},
|
||||
[ThreatVectorType.Idor] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UserInput, CapabilityClass.DatabaseSql],
|
||||
OptionalCapabilities = [CapabilityClass.Authorization],
|
||||
RequiredIntents = [ApplicationIntent.WebServer, ApplicationIntent.RpcServer],
|
||||
EvidencePatterns = ["id", "user", "object", "reference"],
|
||||
BaseWeight = 0.6
|
||||
},
|
||||
[ThreatVectorType.HeaderInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.UserInput, CapabilityClass.NetworkListen],
|
||||
OptionalCapabilities = [],
|
||||
RequiredIntents = [ApplicationIntent.WebServer],
|
||||
EvidencePatterns = ["header", "response", "set"],
|
||||
BaseWeight = 0.5
|
||||
},
|
||||
[ThreatVectorType.LogInjection] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.LogEmit, CapabilityClass.UserInput],
|
||||
OptionalCapabilities = [],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["log", "logger", "print"],
|
||||
BaseWeight = 0.4
|
||||
},
|
||||
[ThreatVectorType.CryptoWeakness] = new()
|
||||
{
|
||||
RequiredCapabilities = [CapabilityClass.CryptoEncrypt],
|
||||
OptionalCapabilities = [CapabilityClass.SecretAccess],
|
||||
RequiredIntents = [],
|
||||
EvidencePatterns = ["md5", "sha1", "des", "ecb", "weak"],
|
||||
BaseWeight = 0.5
|
||||
},
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
private sealed record ThreatVectorRule
|
||||
{
|
||||
public required List<CapabilityClass> RequiredCapabilities { get; init; }
|
||||
public required List<CapabilityClass> OptionalCapabilities { get; init; }
|
||||
public required List<ApplicationIntent> RequiredIntents { get; init; }
|
||||
public required List<string> EvidencePatterns { get; init; }
|
||||
public required double BaseWeight { get; init; }
|
||||
}
|
||||
|
||||
private sealed record RuleMatchResult
|
||||
{
|
||||
public required bool Matches { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public required CapabilityClass MatchedCapabilities { get; init; }
|
||||
public required ImmutableArray<string> Evidence { get; init; }
|
||||
|
||||
public static RuleMatchResult NoMatch => new()
|
||||
{
|
||||
Matches = false,
|
||||
Confidence = 0,
|
||||
MatchedCapabilities = CapabilityClass.None,
|
||||
Evidence = ImmutableArray<string>.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of threat vector inference.
|
||||
/// </summary>
|
||||
public sealed record ThreatInferenceResult
|
||||
{
|
||||
public required ImmutableArray<ThreatVector> ThreatVectors { get; init; }
|
||||
public required double OverallRiskScore { get; init; }
|
||||
public required SemanticConfidence Confidence { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// High-level application intent inferred from entrypoint analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 2).
|
||||
/// Intent classification enables risk prioritization and attack surface modeling.
|
||||
/// </remarks>
|
||||
public enum ApplicationIntent
|
||||
{
|
||||
/// <summary>Intent could not be determined.</summary>
|
||||
Unknown = 0,
|
||||
|
||||
/// <summary>HTTP/HTTPS web server (Django, Express, ASP.NET, etc.).</summary>
|
||||
WebServer = 1,
|
||||
|
||||
/// <summary>Command-line interface tool (Click, Cobra, etc.).</summary>
|
||||
CliTool = 2,
|
||||
|
||||
/// <summary>One-shot batch data processing job.</summary>
|
||||
BatchJob = 3,
|
||||
|
||||
/// <summary>Background job processor (Celery, Sidekiq, etc.).</summary>
|
||||
Worker = 4,
|
||||
|
||||
/// <summary>FaaS handler (Lambda, Azure Functions, Cloud Functions).</summary>
|
||||
Serverless = 5,
|
||||
|
||||
/// <summary>Long-running background daemon service.</summary>
|
||||
Daemon = 6,
|
||||
|
||||
/// <summary>Process manager/init system (systemd, s6, tini).</summary>
|
||||
InitSystem = 7,
|
||||
|
||||
/// <summary>Child process supervisor (supervisord).</summary>
|
||||
Supervisor = 8,
|
||||
|
||||
/// <summary>Database engine (PostgreSQL, MySQL, MongoDB).</summary>
|
||||
DatabaseServer = 9,
|
||||
|
||||
/// <summary>Message broker (RabbitMQ, Kafka, Redis pub/sub).</summary>
|
||||
MessageBroker = 10,
|
||||
|
||||
/// <summary>Cache/session store (Redis, Memcached).</summary>
|
||||
CacheServer = 11,
|
||||
|
||||
/// <summary>Reverse proxy or API gateway (nginx, Envoy, Kong).</summary>
|
||||
ProxyGateway = 12,
|
||||
|
||||
/// <summary>Test framework execution (pytest, jest).</summary>
|
||||
TestRunner = 13,
|
||||
|
||||
/// <summary>Development-only server (hot reload, debug).</summary>
|
||||
DevServer = 14,
|
||||
|
||||
/// <summary>RPC server (gRPC, Thrift, JSON-RPC).</summary>
|
||||
RpcServer = 15,
|
||||
|
||||
/// <summary>GraphQL server (Apollo, Strawberry).</summary>
|
||||
GraphQlServer = 16,
|
||||
|
||||
/// <summary>Stream processor (Kafka Streams, Flink).</summary>
|
||||
StreamProcessor = 17,
|
||||
|
||||
/// <summary>Machine learning inference server.</summary>
|
||||
MlInferenceServer = 18,
|
||||
|
||||
/// <summary>Scheduled task executor (cron, Celery Beat).</summary>
|
||||
ScheduledTask = 19,
|
||||
|
||||
/// <summary>File/object storage server (MinIO, SeaweedFS).</summary>
|
||||
StorageServer = 20,
|
||||
|
||||
/// <summary>Service mesh sidecar (Envoy, Linkerd).</summary>
|
||||
Sidecar = 21,
|
||||
|
||||
/// <summary>Metrics/monitoring collector (Prometheus, Telegraf).</summary>
|
||||
MetricsCollector = 22,
|
||||
|
||||
/// <summary>Log aggregator (Fluentd, Logstash).</summary>
|
||||
LogCollector = 23,
|
||||
|
||||
/// <summary>Container orchestration agent (kubelet, containerd).</summary>
|
||||
ContainerAgent = 24,
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Flags representing capabilities inferred from entrypoint analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 3).
|
||||
/// Capabilities map to potential attack surfaces and threat vectors.
|
||||
/// </remarks>
|
||||
[Flags]
|
||||
public enum CapabilityClass : long
|
||||
{
|
||||
/// <summary>No capabilities detected.</summary>
|
||||
None = 0,
|
||||
|
||||
// Network capabilities
|
||||
/// <summary>Opens listening socket for incoming connections.</summary>
|
||||
NetworkListen = 1L << 0,
|
||||
/// <summary>Makes outbound network connections.</summary>
|
||||
NetworkConnect = 1L << 1,
|
||||
/// <summary>Uses raw sockets or low-level network access.</summary>
|
||||
NetworkRaw = 1L << 2,
|
||||
/// <summary>Performs DNS resolution.</summary>
|
||||
NetworkDns = 1L << 3,
|
||||
|
||||
// Filesystem capabilities
|
||||
/// <summary>Reads from filesystem.</summary>
|
||||
FileRead = 1L << 4,
|
||||
/// <summary>Writes to filesystem.</summary>
|
||||
FileWrite = 1L << 5,
|
||||
/// <summary>Executes files or modifies permissions.</summary>
|
||||
FileExecute = 1L << 6,
|
||||
/// <summary>Watches filesystem for changes.</summary>
|
||||
FileWatch = 1L << 7,
|
||||
|
||||
// Process capabilities
|
||||
/// <summary>Spawns child processes.</summary>
|
||||
ProcessSpawn = 1L << 8,
|
||||
/// <summary>Sends signals to processes.</summary>
|
||||
ProcessSignal = 1L << 9,
|
||||
/// <summary>Uses ptrace or debugging capabilities.</summary>
|
||||
ProcessTrace = 1L << 10,
|
||||
|
||||
// Cryptography capabilities
|
||||
/// <summary>Performs encryption/decryption.</summary>
|
||||
CryptoEncrypt = 1L << 11,
|
||||
/// <summary>Performs signing/verification.</summary>
|
||||
CryptoSign = 1L << 12,
|
||||
/// <summary>Generates cryptographic keys or random numbers.</summary>
|
||||
CryptoKeyGen = 1L << 13,
|
||||
|
||||
// Data store capabilities
|
||||
/// <summary>Accesses relational databases.</summary>
|
||||
DatabaseSql = 1L << 14,
|
||||
/// <summary>Accesses NoSQL databases.</summary>
|
||||
DatabaseNoSql = 1L << 15,
|
||||
/// <summary>Accesses message queues.</summary>
|
||||
MessageQueue = 1L << 16,
|
||||
/// <summary>Accesses cache stores.</summary>
|
||||
CacheAccess = 1L << 17,
|
||||
/// <summary>Accesses object/blob storage.</summary>
|
||||
ObjectStorage = 1L << 18,
|
||||
|
||||
// External service capabilities
|
||||
/// <summary>Makes HTTP API calls to external services.</summary>
|
||||
ExternalHttpApi = 1L << 19,
|
||||
/// <summary>Uses cloud provider SDKs (AWS, GCP, Azure).</summary>
|
||||
CloudSdk = 1L << 20,
|
||||
/// <summary>Sends emails.</summary>
|
||||
EmailSend = 1L << 21,
|
||||
/// <summary>Sends SMS or push notifications.</summary>
|
||||
NotificationSend = 1L << 22,
|
||||
|
||||
// Input/Output capabilities
|
||||
/// <summary>Accepts user input (forms, API bodies).</summary>
|
||||
UserInput = 1L << 23,
|
||||
/// <summary>Processes file uploads.</summary>
|
||||
FileUpload = 1L << 24,
|
||||
/// <summary>Loads configuration files.</summary>
|
||||
ConfigLoad = 1L << 25,
|
||||
/// <summary>Accesses secrets or credentials.</summary>
|
||||
SecretAccess = 1L << 26,
|
||||
/// <summary>Accesses environment variables.</summary>
|
||||
EnvironmentRead = 1L << 27,
|
||||
|
||||
// Observability capabilities
|
||||
/// <summary>Emits structured logs.</summary>
|
||||
LogEmit = 1L << 28,
|
||||
/// <summary>Emits metrics or telemetry.</summary>
|
||||
MetricsEmit = 1L << 29,
|
||||
/// <summary>Emits distributed traces.</summary>
|
||||
TracingEmit = 1L << 30,
|
||||
|
||||
// System capabilities
|
||||
/// <summary>Makes privileged system calls.</summary>
|
||||
SystemPrivileged = 1L << 31,
|
||||
/// <summary>Capabilities enabling container escape.</summary>
|
||||
ContainerEscape = 1L << 32,
|
||||
/// <summary>Loads kernel modules or eBPF programs.</summary>
|
||||
KernelModule = 1L << 33,
|
||||
/// <summary>Modifies system time.</summary>
|
||||
SystemTime = 1L << 34,
|
||||
/// <summary>Modifies network configuration.</summary>
|
||||
NetworkAdmin = 1L << 35,
|
||||
|
||||
// Serialization (security-relevant)
|
||||
/// <summary>Deserializes untrusted data unsafely.</summary>
|
||||
UnsafeDeserialization = 1L << 36,
|
||||
/// <summary>Uses XML parsing with external entities.</summary>
|
||||
XmlExternalEntities = 1L << 37,
|
||||
/// <summary>Evaluates dynamic code (eval, exec).</summary>
|
||||
DynamicCodeEval = 1L << 38,
|
||||
/// <summary>Uses template engines with expression evaluation.</summary>
|
||||
TemplateRendering = 1L << 39,
|
||||
/// <summary>Executes shell commands.</summary>
|
||||
ShellExecution = 1L << 40,
|
||||
|
||||
// Authentication/Authorization
|
||||
/// <summary>Performs authentication operations.</summary>
|
||||
Authentication = 1L << 41,
|
||||
/// <summary>Performs authorization/access control.</summary>
|
||||
Authorization = 1L << 42,
|
||||
/// <summary>Manages sessions or tokens.</summary>
|
||||
SessionManagement = 1L << 43,
|
||||
|
||||
// Convenience combinations
|
||||
/// <summary>Full network access.</summary>
|
||||
NetworkFull = NetworkListen | NetworkConnect,
|
||||
/// <summary>Full filesystem access.</summary>
|
||||
FileSystemFull = FileRead | FileWrite | FileExecute,
|
||||
/// <summary>Any database access.</summary>
|
||||
DatabaseAny = DatabaseSql | DatabaseNoSql,
|
||||
/// <summary>Any cryptographic operation.</summary>
|
||||
CryptoAny = CryptoEncrypt | CryptoSign | CryptoKeyGen,
|
||||
/// <summary>Security-sensitive serialization patterns.</summary>
|
||||
UnsafeSerialization = UnsafeDeserialization | XmlExternalEntities | DynamicCodeEval,
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Types of data flow boundaries in application execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 5).
|
||||
/// </remarks>
|
||||
public enum DataFlowBoundaryType
|
||||
{
|
||||
/// <summary>Incoming HTTP request data.</summary>
|
||||
HttpRequest = 1,
|
||||
/// <summary>Outgoing HTTP response data.</summary>
|
||||
HttpResponse = 2,
|
||||
/// <summary>File input (reading from disk).</summary>
|
||||
FileInput = 3,
|
||||
/// <summary>File output (writing to disk).</summary>
|
||||
FileOutput = 4,
|
||||
/// <summary>SQL query execution.</summary>
|
||||
DatabaseQuery = 5,
|
||||
/// <summary>Database result set.</summary>
|
||||
DatabaseResult = 6,
|
||||
/// <summary>Message queue receive.</summary>
|
||||
MessageReceive = 7,
|
||||
/// <summary>Message queue send.</summary>
|
||||
MessageSend = 8,
|
||||
/// <summary>Environment variable read.</summary>
|
||||
EnvironmentVar = 9,
|
||||
/// <summary>Command-line argument.</summary>
|
||||
CommandLineArg = 10,
|
||||
/// <summary>Standard input stream.</summary>
|
||||
StandardInput = 11,
|
||||
/// <summary>Standard output stream.</summary>
|
||||
StandardOutput = 12,
|
||||
/// <summary>Standard error stream.</summary>
|
||||
StandardError = 13,
|
||||
/// <summary>Network socket read.</summary>
|
||||
SocketRead = 14,
|
||||
/// <summary>Network socket write.</summary>
|
||||
SocketWrite = 15,
|
||||
/// <summary>Process spawn with arguments.</summary>
|
||||
ProcessSpawn = 16,
|
||||
/// <summary>Shared memory read.</summary>
|
||||
SharedMemoryRead = 17,
|
||||
/// <summary>Shared memory write.</summary>
|
||||
SharedMemoryWrite = 18,
|
||||
/// <summary>Cache read operation.</summary>
|
||||
CacheRead = 19,
|
||||
/// <summary>Cache write operation.</summary>
|
||||
CacheWrite = 20,
|
||||
/// <summary>External API call.</summary>
|
||||
ExternalApiCall = 21,
|
||||
/// <summary>External API response.</summary>
|
||||
ExternalApiResponse = 22,
|
||||
/// <summary>Configuration file read.</summary>
|
||||
ConfigRead = 23,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direction of data flow at a boundary.
|
||||
/// </summary>
|
||||
public enum DataFlowDirection
|
||||
{
|
||||
/// <summary>Data entering the application.</summary>
|
||||
Inbound = 1,
|
||||
/// <summary>Data leaving the application.</summary>
|
||||
Outbound = 2,
|
||||
/// <summary>Bidirectional data flow.</summary>
|
||||
Bidirectional = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sensitivity classification for data at boundaries.
|
||||
/// </summary>
|
||||
public enum DataSensitivity
|
||||
{
|
||||
/// <summary>Sensitivity not determined.</summary>
|
||||
Unknown = 0,
|
||||
/// <summary>Public, non-sensitive data.</summary>
|
||||
Public = 1,
|
||||
/// <summary>Internal data, not for external exposure.</summary>
|
||||
Internal = 2,
|
||||
/// <summary>Confidential data requiring protection.</summary>
|
||||
Confidential = 3,
|
||||
/// <summary>Highly restricted data (credentials, keys, PII).</summary>
|
||||
Restricted = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a data flow boundary in the application.
|
||||
/// </summary>
|
||||
public sealed record DataFlowBoundary
|
||||
{
|
||||
/// <summary>Type of boundary.</summary>
|
||||
public required DataFlowBoundaryType Type { get; init; }
|
||||
|
||||
/// <summary>Direction of data flow.</summary>
|
||||
public required DataFlowDirection Direction { get; init; }
|
||||
|
||||
/// <summary>Inferred sensitivity of data at this boundary.</summary>
|
||||
public required DataSensitivity Sensitivity { get; init; }
|
||||
|
||||
/// <summary>Confidence in the boundary detection (0.0-1.0).</summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>Code location where boundary was detected.</summary>
|
||||
public string? Location { get; init; }
|
||||
|
||||
/// <summary>Evidence strings for this boundary detection.</summary>
|
||||
public ImmutableArray<string> Evidence { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>Framework or library providing this boundary.</summary>
|
||||
public string? Framework { get; init; }
|
||||
|
||||
/// <summary>Additional metadata.</summary>
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for DataFlowBoundaryType.
|
||||
/// </summary>
|
||||
public static class DataFlowBoundaryTypeExtensions
|
||||
{
|
||||
/// <summary>Gets the default direction for this boundary type.</summary>
|
||||
public static DataFlowDirection GetDefaultDirection(this DataFlowBoundaryType type) => type switch
|
||||
{
|
||||
DataFlowBoundaryType.HttpRequest => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.HttpResponse => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.FileInput => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.FileOutput => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.DatabaseQuery => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.DatabaseResult => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.MessageReceive => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.MessageSend => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.EnvironmentVar => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.CommandLineArg => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.StandardInput => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.StandardOutput => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.StandardError => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.SocketRead => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.SocketWrite => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.ProcessSpawn => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.SharedMemoryRead => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.SharedMemoryWrite => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.CacheRead => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.CacheWrite => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.ExternalApiCall => DataFlowDirection.Outbound,
|
||||
DataFlowBoundaryType.ExternalApiResponse => DataFlowDirection.Inbound,
|
||||
DataFlowBoundaryType.ConfigRead => DataFlowDirection.Inbound,
|
||||
_ => DataFlowDirection.Bidirectional
|
||||
};
|
||||
|
||||
/// <summary>Determines if this boundary type is security-sensitive by default.</summary>
|
||||
public static bool IsSecuritySensitive(this DataFlowBoundaryType type) => type switch
|
||||
{
|
||||
DataFlowBoundaryType.HttpRequest => true,
|
||||
DataFlowBoundaryType.DatabaseQuery => true,
|
||||
DataFlowBoundaryType.ProcessSpawn => true,
|
||||
DataFlowBoundaryType.CommandLineArg => true,
|
||||
DataFlowBoundaryType.EnvironmentVar => true,
|
||||
DataFlowBoundaryType.ExternalApiCall => true,
|
||||
DataFlowBoundaryType.ConfigRead => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for semantic entrypoint analyzers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 7).
|
||||
/// Implementations analyze entrypoints to infer intent, capabilities, and attack surface.
|
||||
/// </remarks>
|
||||
public interface ISemanticEntrypointAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzes an entrypoint to produce semantic understanding.
|
||||
/// </summary>
|
||||
/// <param name="context">Analysis context with entrypoint and language data.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Semantic entrypoint analysis result.</returns>
|
||||
Task<SemanticEntrypoint> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the languages this analyzer supports.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> SupportedLanguages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority of this analyzer (higher = processed first).
|
||||
/// </summary>
|
||||
int Priority => 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for semantic analysis containing all relevant data.
|
||||
/// </summary>
|
||||
public sealed record SemanticAnalysisContext
|
||||
{
|
||||
/// <summary>The entrypoint specification to analyze.</summary>
|
||||
public required EntrypointSpecification Specification { get; init; }
|
||||
|
||||
/// <summary>Entry trace result from initial analysis.</summary>
|
||||
public required EntryTraceResult EntryTraceResult { get; init; }
|
||||
|
||||
/// <summary>Root filesystem accessor.</summary>
|
||||
public required IRootFileSystem FileSystem { get; init; }
|
||||
|
||||
/// <summary>Detected primary language.</summary>
|
||||
public string? PrimaryLanguage { get; init; }
|
||||
|
||||
/// <summary>All detected languages in the image.</summary>
|
||||
public IReadOnlyList<string> DetectedLanguages { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>Package manager manifests found.</summary>
|
||||
public IReadOnlyDictionary<string, string> ManifestPaths { get; init; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Import/dependency information from language analyzers.</summary>
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Dependencies { get; init; } = new Dictionary<string, IReadOnlyList<string>>();
|
||||
|
||||
/// <summary>Image digest for correlation.</summary>
|
||||
public string? ImageDigest { get; init; }
|
||||
|
||||
/// <summary>Scan ID for tracing.</summary>
|
||||
public string? ScanId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of semantic analysis that can be partial/incremental.
|
||||
/// </summary>
|
||||
public sealed record SemanticAnalysisResult
|
||||
{
|
||||
/// <summary>Whether analysis completed successfully.</summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>The semantic entrypoint if successful.</summary>
|
||||
public SemanticEntrypoint? Entrypoint { get; init; }
|
||||
|
||||
/// <summary>Partial results if analysis was incomplete.</summary>
|
||||
public PartialSemanticResult? PartialResult { get; init; }
|
||||
|
||||
/// <summary>Diagnostics from analysis.</summary>
|
||||
public IReadOnlyList<SemanticDiagnostic> Diagnostics { get; init; } = Array.Empty<SemanticDiagnostic>();
|
||||
|
||||
/// <summary>Creates successful result.</summary>
|
||||
public static SemanticAnalysisResult Successful(SemanticEntrypoint entrypoint) => new()
|
||||
{
|
||||
Success = true,
|
||||
Entrypoint = entrypoint
|
||||
};
|
||||
|
||||
/// <summary>Creates failed result with diagnostics.</summary>
|
||||
public static SemanticAnalysisResult Failed(params SemanticDiagnostic[] diagnostics) => new()
|
||||
{
|
||||
Success = false,
|
||||
Diagnostics = diagnostics
|
||||
};
|
||||
|
||||
/// <summary>Creates partial result.</summary>
|
||||
public static SemanticAnalysisResult Partial(PartialSemanticResult partial, params SemanticDiagnostic[] diagnostics) => new()
|
||||
{
|
||||
Success = false,
|
||||
PartialResult = partial,
|
||||
Diagnostics = diagnostics
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Partial semantic analysis results when full analysis isn't possible.
|
||||
/// </summary>
|
||||
public sealed record PartialSemanticResult
|
||||
{
|
||||
/// <summary>Inferred intent if determined.</summary>
|
||||
public ApplicationIntent? Intent { get; init; }
|
||||
|
||||
/// <summary>Capabilities detected so far.</summary>
|
||||
public CapabilityClass Capabilities { get; init; } = CapabilityClass.None;
|
||||
|
||||
/// <summary>Confidence in partial results.</summary>
|
||||
public SemanticConfidence? Confidence { get; init; }
|
||||
|
||||
/// <summary>Reason analysis couldn't complete.</summary>
|
||||
public string? IncompleteReason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic from semantic analysis.
|
||||
/// </summary>
|
||||
public sealed record SemanticDiagnostic
|
||||
{
|
||||
/// <summary>Severity of the diagnostic.</summary>
|
||||
public required DiagnosticSeverity Severity { get; init; }
|
||||
|
||||
/// <summary>Diagnostic code.</summary>
|
||||
public required string Code { get; init; }
|
||||
|
||||
/// <summary>Human-readable message.</summary>
|
||||
public required string Message { get; init; }
|
||||
|
||||
/// <summary>Location in code if applicable.</summary>
|
||||
public string? Location { get; init; }
|
||||
|
||||
/// <summary>Creates info diagnostic.</summary>
|
||||
public static SemanticDiagnostic Info(string code, string message, string? location = null) => new()
|
||||
{
|
||||
Severity = DiagnosticSeverity.Info,
|
||||
Code = code,
|
||||
Message = message,
|
||||
Location = location
|
||||
};
|
||||
|
||||
/// <summary>Creates warning diagnostic.</summary>
|
||||
public static SemanticDiagnostic Warning(string code, string message, string? location = null) => new()
|
||||
{
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Code = code,
|
||||
Message = message,
|
||||
Location = location
|
||||
};
|
||||
|
||||
/// <summary>Creates error diagnostic.</summary>
|
||||
public static SemanticDiagnostic Error(string code, string message, string? location = null) => new()
|
||||
{
|
||||
Severity = DiagnosticSeverity.Error,
|
||||
Code = code,
|
||||
Message = message,
|
||||
Location = location
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Severity levels for semantic diagnostics.
|
||||
/// </summary>
|
||||
public enum DiagnosticSeverity
|
||||
{
|
||||
/// <summary>Informational.</summary>
|
||||
Info = 0,
|
||||
/// <summary>Warning.</summary>
|
||||
Warning = 1,
|
||||
/// <summary>Error.</summary>
|
||||
Error = 2,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for IRootFileSystem to support semantic analysis.
|
||||
/// </summary>
|
||||
public static class RootFileSystemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously checks if a directory exists.
|
||||
/// </summary>
|
||||
public static Task<bool> DirectoryExistsAsync(this IRootFileSystem fs, string path, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(fs.DirectoryExists(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously lists files matching a pattern in a directory.
|
||||
/// </summary>
|
||||
public static Task<IReadOnlyList<string>> ListFilesAsync(
|
||||
this IRootFileSystem fs,
|
||||
string path,
|
||||
string pattern,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
if (!fs.DirectoryExists(path))
|
||||
return Task.FromResult<IReadOnlyList<string>>(results);
|
||||
|
||||
var entries = fs.EnumerateDirectory(path);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
continue;
|
||||
|
||||
var fileName = Path.GetFileName(entry.Path);
|
||||
if (MatchesPattern(fileName, pattern))
|
||||
{
|
||||
results.Add(entry.Path);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IReadOnlyList<string>>(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads a file as text.
|
||||
/// </summary>
|
||||
public static Task<string> ReadFileAsync(
|
||||
this IRootFileSystem fs,
|
||||
string path,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (fs.TryReadAllText(path, out _, out var content))
|
||||
{
|
||||
return Task.FromResult(content);
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"File not found: {path}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously tries to read a file as text.
|
||||
/// </summary>
|
||||
public static Task<string?> TryReadFileAsync(
|
||||
this IRootFileSystem fs,
|
||||
string path,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (fs.TryReadAllText(path, out _, out var content))
|
||||
{
|
||||
return Task.FromResult<string?>(content);
|
||||
}
|
||||
|
||||
return Task.FromResult<string?>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a file exists.
|
||||
/// </summary>
|
||||
public static bool FileExists(this IRootFileSystem fs, string path)
|
||||
{
|
||||
return fs.TryReadBytes(path, 0, out _, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously checks if a file exists.
|
||||
/// </summary>
|
||||
public static Task<bool> FileExistsAsync(this IRootFileSystem fs, string path, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(fs.FileExists(path));
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string fileName, string pattern)
|
||||
{
|
||||
// Simple glob pattern matching (supports * and ?)
|
||||
if (string.IsNullOrEmpty(pattern))
|
||||
return true;
|
||||
|
||||
if (pattern == "*")
|
||||
return true;
|
||||
|
||||
// Handle *.ext pattern
|
||||
if (pattern.StartsWith("*."))
|
||||
{
|
||||
var ext = pattern[1..]; // Include the dot
|
||||
return fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Handle prefix* pattern
|
||||
if (pattern.EndsWith("*"))
|
||||
{
|
||||
var prefix = pattern[..^1];
|
||||
return fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Exact match
|
||||
return fileName.Equals(pattern, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Confidence tier for semantic inference.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 6).
|
||||
/// </remarks>
|
||||
public enum ConfidenceTier
|
||||
{
|
||||
/// <summary>Cannot determine with available evidence.</summary>
|
||||
Unknown = 0,
|
||||
/// <summary>Low confidence; heuristic-based with limited signals.</summary>
|
||||
Low = 1,
|
||||
/// <summary>Medium confidence; multiple signals agree.</summary>
|
||||
Medium = 2,
|
||||
/// <summary>High confidence; strong evidence from framework patterns.</summary>
|
||||
High = 3,
|
||||
/// <summary>Definitive; explicit declaration or unambiguous signature.</summary>
|
||||
Definitive = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents confidence in a semantic inference with supporting evidence.
|
||||
/// </summary>
|
||||
public sealed record SemanticConfidence
|
||||
{
|
||||
/// <summary>Numeric confidence score (0.0-1.0).</summary>
|
||||
public required double Score { get; init; }
|
||||
|
||||
/// <summary>Confidence tier classification.</summary>
|
||||
public required ConfidenceTier Tier { get; init; }
|
||||
|
||||
/// <summary>Chain of reasoning that led to this confidence.</summary>
|
||||
public required ImmutableArray<string> ReasoningChain { get; init; }
|
||||
|
||||
/// <summary>Number of signals that contributed to this inference.</summary>
|
||||
public int SignalCount { get; init; }
|
||||
|
||||
/// <summary>Whether conflicting signals were detected.</summary>
|
||||
public bool HasConflicts { get; init; }
|
||||
|
||||
/// <summary>Creates unknown confidence.</summary>
|
||||
public static SemanticConfidence Unknown() => new()
|
||||
{
|
||||
Score = 0.0,
|
||||
Tier = ConfidenceTier.Unknown,
|
||||
ReasoningChain = ImmutableArray.Create("No signals detected"),
|
||||
SignalCount = 0,
|
||||
HasConflicts = false
|
||||
};
|
||||
|
||||
/// <summary>Creates low confidence with reasoning.</summary>
|
||||
public static SemanticConfidence Low(params string[] reasons) => new()
|
||||
{
|
||||
Score = 0.25,
|
||||
Tier = ConfidenceTier.Low,
|
||||
ReasoningChain = reasons.ToImmutableArray(),
|
||||
SignalCount = reasons.Length,
|
||||
HasConflicts = false
|
||||
};
|
||||
|
||||
/// <summary>Creates medium confidence with reasoning.</summary>
|
||||
public static SemanticConfidence Medium(params string[] reasons) => new()
|
||||
{
|
||||
Score = 0.5,
|
||||
Tier = ConfidenceTier.Medium,
|
||||
ReasoningChain = reasons.ToImmutableArray(),
|
||||
SignalCount = reasons.Length,
|
||||
HasConflicts = false
|
||||
};
|
||||
|
||||
/// <summary>Creates high confidence with reasoning.</summary>
|
||||
public static SemanticConfidence High(params string[] reasons) => new()
|
||||
{
|
||||
Score = 0.75,
|
||||
Tier = ConfidenceTier.High,
|
||||
ReasoningChain = reasons.ToImmutableArray(),
|
||||
SignalCount = reasons.Length,
|
||||
HasConflicts = false
|
||||
};
|
||||
|
||||
/// <summary>Creates definitive confidence with reasoning.</summary>
|
||||
public static SemanticConfidence Definitive(params string[] reasons) => new()
|
||||
{
|
||||
Score = 1.0,
|
||||
Tier = ConfidenceTier.Definitive,
|
||||
ReasoningChain = reasons.ToImmutableArray(),
|
||||
SignalCount = reasons.Length,
|
||||
HasConflicts = false
|
||||
};
|
||||
|
||||
/// <summary>Creates confidence from score with auto-tiering.</summary>
|
||||
public static SemanticConfidence FromScore(double score, ImmutableArray<string> reasoning, bool hasConflicts = false)
|
||||
{
|
||||
var tier = score switch
|
||||
{
|
||||
>= 0.95 => ConfidenceTier.Definitive,
|
||||
>= 0.70 => ConfidenceTier.High,
|
||||
>= 0.40 => ConfidenceTier.Medium,
|
||||
>= 0.15 => ConfidenceTier.Low,
|
||||
_ => ConfidenceTier.Unknown
|
||||
};
|
||||
|
||||
return new()
|
||||
{
|
||||
Score = Math.Clamp(score, 0.0, 1.0),
|
||||
Tier = tier,
|
||||
ReasoningChain = reasoning,
|
||||
SignalCount = reasoning.Length,
|
||||
HasConflicts = hasConflicts
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Combines multiple confidence values with weighted average.</summary>
|
||||
public static SemanticConfidence Combine(IEnumerable<SemanticConfidence> confidences)
|
||||
{
|
||||
var list = confidences.ToList();
|
||||
if (list.Count == 0)
|
||||
return Unknown();
|
||||
|
||||
var totalScore = list.Sum(c => c.Score);
|
||||
var avgScore = totalScore / list.Count;
|
||||
var allReasons = list.SelectMany(c => c.ReasoningChain).ToImmutableArray();
|
||||
var hasConflicts = list.Any(c => c.HasConflicts) || HasConflictingTiers(list);
|
||||
|
||||
return FromScore(avgScore, allReasons, hasConflicts);
|
||||
}
|
||||
|
||||
private static bool HasConflictingTiers(List<SemanticConfidence> confidences)
|
||||
{
|
||||
if (confidences.Count < 2)
|
||||
return false;
|
||||
|
||||
var tiers = confidences.Select(c => c.Tier).Distinct().ToList();
|
||||
return tiers.Count > 1 && tiers.Max() - tiers.Min() > 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Entry trace analyzer with integrated semantic analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 17).
|
||||
/// Wraps the base EntryTraceAnalyzer and adds semantic understanding.
|
||||
/// </remarks>
|
||||
public sealed class SemanticEntryTraceAnalyzer : ISemanticEntryTraceAnalyzer
|
||||
{
|
||||
private readonly IEntryTraceAnalyzer _baseAnalyzer;
|
||||
private readonly SemanticEntrypointOrchestrator _orchestrator;
|
||||
private readonly ILogger<SemanticEntryTraceAnalyzer> _logger;
|
||||
|
||||
public SemanticEntryTraceAnalyzer(
|
||||
IEntryTraceAnalyzer baseAnalyzer,
|
||||
SemanticEntrypointOrchestrator orchestrator,
|
||||
ILogger<SemanticEntryTraceAnalyzer> logger)
|
||||
{
|
||||
_baseAnalyzer = baseAnalyzer ?? throw new ArgumentNullException(nameof(baseAnalyzer));
|
||||
_orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public SemanticEntryTraceAnalyzer(
|
||||
IEntryTraceAnalyzer baseAnalyzer,
|
||||
ILogger<SemanticEntryTraceAnalyzer> logger)
|
||||
: this(baseAnalyzer, new SemanticEntrypointOrchestrator(), logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<SemanticEntryTraceResult> ResolveWithSemanticsAsync(
|
||||
EntryTrace.EntrypointSpecification entrypoint,
|
||||
EntryTraceContext context,
|
||||
ContainerMetadata? containerMetadata = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entrypoint);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
// Step 1: Run base entry trace analysis
|
||||
_logger.LogDebug("Starting entry trace resolution for scan {ScanId}", context.ScanId);
|
||||
var graph = await _baseAnalyzer.ResolveAsync(entrypoint, context, cancellationToken);
|
||||
|
||||
// Step 2: Build the full entry trace result
|
||||
var traceResult = new EntryTraceResult(
|
||||
context.ScanId,
|
||||
context.ImageDigest,
|
||||
DateTimeOffset.UtcNow,
|
||||
graph,
|
||||
SerializeToNdjson(graph));
|
||||
|
||||
// Step 3: Run semantic analysis
|
||||
_logger.LogDebug("Starting semantic analysis for scan {ScanId}", context.ScanId);
|
||||
SemanticEntrypoint? semanticResult = null;
|
||||
SemanticAnalysisResult? analysisResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
var semanticContext = CreateSemanticContext(
|
||||
traceResult,
|
||||
context.FileSystem,
|
||||
containerMetadata);
|
||||
|
||||
analysisResult = await _orchestrator.AnalyzeAsync(semanticContext, cancellationToken);
|
||||
|
||||
if (analysisResult.Success && analysisResult.Entrypoint is not null)
|
||||
{
|
||||
semanticResult = analysisResult.Entrypoint;
|
||||
_logger.LogInformation(
|
||||
"Semantic analysis complete for scan {ScanId}: Intent={Intent}, Capabilities={CapCount}, Threats={ThreatCount}",
|
||||
context.ScanId,
|
||||
semanticResult.Intent,
|
||||
CountCapabilities(semanticResult.Capabilities),
|
||||
semanticResult.AttackSurface.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Semantic analysis incomplete for scan {ScanId}: {DiagnosticCount} diagnostics",
|
||||
context.ScanId,
|
||||
analysisResult.Diagnostics.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex, "Semantic analysis failed for scan {ScanId}", context.ScanId);
|
||||
}
|
||||
|
||||
return new SemanticEntryTraceResult
|
||||
{
|
||||
TraceResult = traceResult,
|
||||
SemanticEntrypoint = semanticResult,
|
||||
AnalysisResult = analysisResult,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<EntryTraceGraph> ResolveAsync(
|
||||
EntryTrace.EntrypointSpecification entrypoint,
|
||||
EntryTraceContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _baseAnalyzer.ResolveAsync(entrypoint, context, cancellationToken);
|
||||
}
|
||||
|
||||
private SemanticAnalysisContext CreateSemanticContext(
|
||||
EntryTraceResult traceResult,
|
||||
IRootFileSystem fileSystem,
|
||||
ContainerMetadata? containerMetadata)
|
||||
{
|
||||
var metadata = containerMetadata ?? ContainerMetadata.Empty;
|
||||
|
||||
// Convert base EntrypointSpecification to semantic version
|
||||
var plan = traceResult.Graph.Plans.FirstOrDefault();
|
||||
var spec = new Semantic.EntrypointSpecification
|
||||
{
|
||||
Entrypoint = plan?.Command ?? ImmutableArray<string>.Empty,
|
||||
Cmd = ImmutableArray<string>.Empty,
|
||||
WorkingDirectory = plan?.WorkingDirectory,
|
||||
User = plan?.User,
|
||||
Shell = metadata.Shell,
|
||||
Environment = metadata.Environment?.ToImmutableDictionary(),
|
||||
ExposedPorts = metadata.ExposedPorts,
|
||||
Volumes = metadata.Volumes,
|
||||
Labels = metadata.Labels?.ToImmutableDictionary(),
|
||||
ImageDigest = traceResult.ImageDigest,
|
||||
ImageReference = metadata.ImageReference
|
||||
};
|
||||
|
||||
return new SemanticAnalysisContext
|
||||
{
|
||||
Specification = spec,
|
||||
EntryTraceResult = traceResult,
|
||||
FileSystem = fileSystem,
|
||||
PrimaryLanguage = InferPrimaryLanguage(traceResult),
|
||||
DetectedLanguages = InferDetectedLanguages(traceResult),
|
||||
ManifestPaths = metadata.ManifestPaths ?? new Dictionary<string, string>(),
|
||||
Dependencies = metadata.Dependencies ?? new Dictionary<string, IReadOnlyList<string>>(),
|
||||
ImageDigest = traceResult.ImageDigest,
|
||||
ScanId = traceResult.ScanId
|
||||
};
|
||||
}
|
||||
|
||||
private static string? InferPrimaryLanguage(EntryTraceResult result)
|
||||
{
|
||||
var terminal = result.Graph.Terminals.FirstOrDefault();
|
||||
if (terminal?.Runtime is not null)
|
||||
{
|
||||
return terminal.Runtime.ToLowerInvariant() switch
|
||||
{
|
||||
var r when r.Contains("python") => "python",
|
||||
var r when r.Contains("node") => "node",
|
||||
var r when r.Contains("java") => "java",
|
||||
var r when r.Contains("dotnet") || r.Contains(".net") => "dotnet",
|
||||
var r when r.Contains("go") => "go",
|
||||
_ => terminal.Runtime
|
||||
};
|
||||
}
|
||||
|
||||
var interpreterNode = result.Graph.Nodes.FirstOrDefault(n => n.Kind == EntryTraceNodeKind.Interpreter);
|
||||
return interpreterNode?.InterpreterKind switch
|
||||
{
|
||||
EntryTraceInterpreterKind.Python => "python",
|
||||
EntryTraceInterpreterKind.Node => "node",
|
||||
EntryTraceInterpreterKind.Java => "java",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> InferDetectedLanguages(EntryTraceResult result)
|
||||
{
|
||||
var languages = new HashSet<string>();
|
||||
|
||||
foreach (var terminal in result.Graph.Terminals)
|
||||
{
|
||||
if (terminal.Runtime is not null)
|
||||
{
|
||||
var lang = terminal.Runtime.ToLowerInvariant() switch
|
||||
{
|
||||
var r when r.Contains("python") => "python",
|
||||
var r when r.Contains("node") => "node",
|
||||
var r when r.Contains("java") => "java",
|
||||
var r when r.Contains("dotnet") => "dotnet",
|
||||
var r when r.Contains("go") => "go",
|
||||
var r when r.Contains("ruby") => "ruby",
|
||||
var r when r.Contains("rust") => "rust",
|
||||
_ => null
|
||||
};
|
||||
if (lang is not null) languages.Add(lang);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in result.Graph.Nodes)
|
||||
{
|
||||
var lang = node.InterpreterKind switch
|
||||
{
|
||||
EntryTraceInterpreterKind.Python => "python",
|
||||
EntryTraceInterpreterKind.Node => "node",
|
||||
EntryTraceInterpreterKind.Java => "java",
|
||||
_ => null
|
||||
};
|
||||
if (lang is not null) languages.Add(lang);
|
||||
}
|
||||
|
||||
return languages.ToList();
|
||||
}
|
||||
|
||||
private static int CountCapabilities(CapabilityClass caps)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && !IsCompositeFlag(flag) && caps.HasFlag(flag))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static bool IsCompositeFlag(CapabilityClass flag)
|
||||
{
|
||||
var val = (long)flag;
|
||||
return val != 0 && (val & (val - 1)) != 0;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> SerializeToNdjson(EntryTraceGraph graph)
|
||||
{
|
||||
// Simplified serialization - full implementation would use proper JSON serialization
|
||||
var lines = new List<string>();
|
||||
|
||||
foreach (var node in graph.Nodes)
|
||||
{
|
||||
lines.Add($"{{\"type\":\"node\",\"id\":{node.Id},\"kind\":\"{node.Kind}\",\"name\":\"{node.DisplayName}\"}}");
|
||||
}
|
||||
|
||||
foreach (var edge in graph.Edges)
|
||||
{
|
||||
lines.Add($"{{\"type\":\"edge\",\"from\":{edge.FromNodeId},\"to\":{edge.ToNodeId},\"rel\":\"{edge.Relationship}\"}}");
|
||||
}
|
||||
|
||||
return lines.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for semantic-aware entry trace analysis.
|
||||
/// </summary>
|
||||
public interface ISemanticEntryTraceAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves entrypoint graph (delegates to base analyzer).
|
||||
/// </summary>
|
||||
ValueTask<EntryTraceGraph> ResolveAsync(
|
||||
EntryTrace.EntrypointSpecification entrypoint,
|
||||
EntryTraceContext context,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves entrypoint and performs semantic analysis.
|
||||
/// </summary>
|
||||
ValueTask<SemanticEntryTraceResult> ResolveWithSemanticsAsync(
|
||||
EntryTrace.EntrypointSpecification entrypoint,
|
||||
EntryTraceContext context,
|
||||
ContainerMetadata? containerMetadata = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combined result of entry trace resolution and semantic analysis.
|
||||
/// </summary>
|
||||
public sealed record SemanticEntryTraceResult
|
||||
{
|
||||
/// <summary>Base entry trace result.</summary>
|
||||
public required EntryTraceResult TraceResult { get; init; }
|
||||
|
||||
/// <summary>Semantic analysis result, if successful.</summary>
|
||||
public SemanticEntrypoint? SemanticEntrypoint { get; init; }
|
||||
|
||||
/// <summary>Full analysis result with diagnostics.</summary>
|
||||
public SemanticAnalysisResult? AnalysisResult { get; init; }
|
||||
|
||||
/// <summary>When the analysis was performed.</summary>
|
||||
public required DateTimeOffset AnalyzedAt { get; init; }
|
||||
|
||||
/// <summary>Whether semantic analysis succeeded.</summary>
|
||||
public bool HasSemantics => SemanticEntrypoint is not null;
|
||||
|
||||
/// <summary>Quick access to inferred intent.</summary>
|
||||
public ApplicationIntent Intent => SemanticEntrypoint?.Intent ?? ApplicationIntent.Unknown;
|
||||
|
||||
/// <summary>Quick access to detected capabilities.</summary>
|
||||
public CapabilityClass Capabilities => SemanticEntrypoint?.Capabilities ?? CapabilityClass.None;
|
||||
|
||||
/// <summary>Quick access to attack surface.</summary>
|
||||
public ImmutableArray<ThreatVector> AttackSurface =>
|
||||
SemanticEntrypoint?.AttackSurface ?? ImmutableArray<ThreatVector>.Empty;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entrypoint with semantic understanding of intent, capabilities, and attack surface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 1).
|
||||
/// This is the core record that captures semantic analysis results for an entrypoint.
|
||||
/// </remarks>
|
||||
public sealed record SemanticEntrypoint
|
||||
{
|
||||
/// <summary>Unique identifier for this semantic analysis result.</summary>
|
||||
public required string Id { get; init; }
|
||||
|
||||
/// <summary>Reference to the underlying entrypoint specification.</summary>
|
||||
public required EntrypointSpecification Specification { get; init; }
|
||||
|
||||
/// <summary>Inferred application intent.</summary>
|
||||
public required ApplicationIntent Intent { get; init; }
|
||||
|
||||
/// <summary>Inferred capabilities (flags).</summary>
|
||||
public required CapabilityClass Capabilities { get; init; }
|
||||
|
||||
/// <summary>Identified threat vectors with confidence.</summary>
|
||||
public required ImmutableArray<ThreatVector> AttackSurface { get; init; }
|
||||
|
||||
/// <summary>Data flow boundaries detected.</summary>
|
||||
public required ImmutableArray<DataFlowBoundary> DataBoundaries { get; init; }
|
||||
|
||||
/// <summary>Overall confidence in the semantic analysis.</summary>
|
||||
public required SemanticConfidence Confidence { get; init; }
|
||||
|
||||
/// <summary>Language of the primary entrypoint code.</summary>
|
||||
public string? Language { get; init; }
|
||||
|
||||
/// <summary>Framework detected (e.g., "Django", "Spring Boot", "Express").</summary>
|
||||
public string? Framework { get; init; }
|
||||
|
||||
/// <summary>Framework version if detected.</summary>
|
||||
public string? FrameworkVersion { get; init; }
|
||||
|
||||
/// <summary>Runtime version if detected.</summary>
|
||||
public string? RuntimeVersion { get; init; }
|
||||
|
||||
/// <summary>Additional metadata.</summary>
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>Timestamp when analysis was performed (UTC ISO-8601).</summary>
|
||||
public required string AnalyzedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specification of the entrypoint being analyzed.
|
||||
/// </summary>
|
||||
public sealed record EntrypointSpecification
|
||||
{
|
||||
/// <summary>Container ENTRYPOINT command array.</summary>
|
||||
public ImmutableArray<string> Entrypoint { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>Container CMD command array.</summary>
|
||||
public ImmutableArray<string> Cmd { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>Working directory for entrypoint execution.</summary>
|
||||
public string? WorkingDirectory { get; init; }
|
||||
|
||||
/// <summary>User context for execution.</summary>
|
||||
public string? User { get; init; }
|
||||
|
||||
/// <summary>Shell used for shell-form commands.</summary>
|
||||
public string? Shell { get; init; }
|
||||
|
||||
/// <summary>Environment variables set in the image.</summary>
|
||||
public ImmutableDictionary<string, string>? Environment { get; init; }
|
||||
|
||||
/// <summary>Exposed ports in the image.</summary>
|
||||
public ImmutableArray<int> ExposedPorts { get; init; } = ImmutableArray<int>.Empty;
|
||||
|
||||
/// <summary>Volumes defined in the image.</summary>
|
||||
public ImmutableArray<string> Volumes { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>Labels set in the image.</summary>
|
||||
public ImmutableDictionary<string, string>? Labels { get; init; }
|
||||
|
||||
/// <summary>Image digest (sha256).</summary>
|
||||
public string? ImageDigest { get; init; }
|
||||
|
||||
/// <summary>Image reference (registry/repo:tag).</summary>
|
||||
public string? ImageReference { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating SemanticEntrypoint instances.
|
||||
/// </summary>
|
||||
public sealed class SemanticEntrypointBuilder
|
||||
{
|
||||
private string? _id;
|
||||
private EntrypointSpecification? _specification;
|
||||
private ApplicationIntent _intent = ApplicationIntent.Unknown;
|
||||
private CapabilityClass _capabilities = CapabilityClass.None;
|
||||
private readonly List<ThreatVector> _attackSurface = new();
|
||||
private readonly List<DataFlowBoundary> _dataBoundaries = new();
|
||||
private SemanticConfidence? _confidence;
|
||||
private string? _language;
|
||||
private string? _framework;
|
||||
private string? _frameworkVersion;
|
||||
private string? _runtimeVersion;
|
||||
private readonly Dictionary<string, string> _metadata = new();
|
||||
|
||||
public SemanticEntrypointBuilder WithId(string id)
|
||||
{
|
||||
_id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithSpecification(EntrypointSpecification specification)
|
||||
{
|
||||
_specification = specification;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithIntent(ApplicationIntent intent)
|
||||
{
|
||||
_intent = intent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithCapabilities(CapabilityClass capabilities)
|
||||
{
|
||||
_capabilities = capabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder AddCapability(CapabilityClass capability)
|
||||
{
|
||||
_capabilities |= capability;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder AddThreatVector(ThreatVector vector)
|
||||
{
|
||||
_attackSurface.Add(vector);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder AddDataBoundary(DataFlowBoundary boundary)
|
||||
{
|
||||
_dataBoundaries.Add(boundary);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithConfidence(SemanticConfidence confidence)
|
||||
{
|
||||
_confidence = confidence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithLanguage(string language)
|
||||
{
|
||||
_language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithFramework(string framework, string? version = null)
|
||||
{
|
||||
_framework = framework;
|
||||
_frameworkVersion = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder WithRuntimeVersion(string version)
|
||||
{
|
||||
_runtimeVersion = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypointBuilder AddMetadata(string key, string value)
|
||||
{
|
||||
_metadata[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SemanticEntrypoint Build()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_id))
|
||||
throw new InvalidOperationException("Id is required");
|
||||
if (_specification is null)
|
||||
throw new InvalidOperationException("Specification is required");
|
||||
|
||||
return new SemanticEntrypoint
|
||||
{
|
||||
Id = _id,
|
||||
Specification = _specification,
|
||||
Intent = _intent,
|
||||
Capabilities = _capabilities,
|
||||
AttackSurface = _attackSurface.ToImmutableArray(),
|
||||
DataBoundaries = _dataBoundaries.ToImmutableArray(),
|
||||
Confidence = _confidence ?? SemanticConfidence.Unknown(),
|
||||
Language = _language,
|
||||
Framework = _framework,
|
||||
FrameworkVersion = _frameworkVersion,
|
||||
RuntimeVersion = _runtimeVersion,
|
||||
Metadata = _metadata.Count > 0 ? _metadata.ToImmutableDictionary() : null,
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.EntryTrace.FileSystem;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic.Analysis;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates semantic analysis by composing adapters, detectors, and inferrers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 16).
|
||||
/// Provides unified semantic analysis pipeline for all supported languages.
|
||||
/// </remarks>
|
||||
public sealed class SemanticEntrypointOrchestrator
|
||||
{
|
||||
private readonly IReadOnlyList<ISemanticEntrypointAnalyzer> _adapters;
|
||||
private readonly CapabilityDetector _capabilityDetector;
|
||||
private readonly ThreatVectorInferrer _threatInferrer;
|
||||
private readonly DataBoundaryMapper _boundaryMapper;
|
||||
|
||||
public SemanticEntrypointOrchestrator()
|
||||
: this(CreateDefaultAdapters(), new CapabilityDetector(), new ThreatVectorInferrer(), new DataBoundaryMapper())
|
||||
{
|
||||
}
|
||||
|
||||
public SemanticEntrypointOrchestrator(
|
||||
IReadOnlyList<ISemanticEntrypointAnalyzer> adapters,
|
||||
CapabilityDetector capabilityDetector,
|
||||
ThreatVectorInferrer threatInferrer,
|
||||
DataBoundaryMapper boundaryMapper)
|
||||
{
|
||||
_adapters = adapters.OrderByDescending(a => a.Priority).ToList();
|
||||
_capabilityDetector = capabilityDetector;
|
||||
_threatInferrer = threatInferrer;
|
||||
_boundaryMapper = boundaryMapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs full semantic analysis on an entrypoint.
|
||||
/// </summary>
|
||||
public async Task<SemanticAnalysisResult> AnalyzeAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var diagnostics = new List<SemanticDiagnostic>();
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Run capability detection
|
||||
var capabilityResult = _capabilityDetector.Detect(context);
|
||||
diagnostics.Add(SemanticDiagnostic.Info(
|
||||
"CAP-001",
|
||||
$"Detected {CountCapabilities(capabilityResult.Capabilities)} capabilities"));
|
||||
|
||||
// Step 2: Find matching language adapter
|
||||
var adapter = FindAdapter(context);
|
||||
if (adapter is null)
|
||||
{
|
||||
diagnostics.Add(SemanticDiagnostic.Warning(
|
||||
"ADAPT-001",
|
||||
$"No adapter found for language: {context.PrimaryLanguage ?? "unknown"}"));
|
||||
|
||||
// Return partial result with just capability detection
|
||||
return CreatePartialResult(context, capabilityResult, diagnostics);
|
||||
}
|
||||
|
||||
// Step 3: Run language-specific adapter
|
||||
var adapterResult = await adapter.AnalyzeAsync(context, cancellationToken);
|
||||
diagnostics.Add(SemanticDiagnostic.Info(
|
||||
"ADAPT-002",
|
||||
$"Adapter {adapter.GetType().Name} inferred intent: {adapterResult.Intent}"));
|
||||
|
||||
// Step 4: Merge capabilities from adapter and detector
|
||||
var mergedCapabilities = adapterResult.Capabilities | capabilityResult.Capabilities;
|
||||
|
||||
// Step 5: Run threat vector inference
|
||||
var threatResult = _threatInferrer.Infer(
|
||||
mergedCapabilities,
|
||||
adapterResult.Intent,
|
||||
capabilityResult.Evidence.ToList());
|
||||
diagnostics.Add(SemanticDiagnostic.Info(
|
||||
"THREAT-001",
|
||||
$"Inferred {threatResult.ThreatVectors.Length} threat vectors, risk score: {threatResult.OverallRiskScore:P0}"));
|
||||
|
||||
// Step 6: Map data boundaries
|
||||
var boundaryResult = _boundaryMapper.Map(
|
||||
context,
|
||||
adapterResult.Intent,
|
||||
mergedCapabilities,
|
||||
capabilityResult.Evidence.ToList());
|
||||
diagnostics.Add(SemanticDiagnostic.Info(
|
||||
"BOUND-001",
|
||||
$"Mapped {boundaryResult.Boundaries.Length} data boundaries " +
|
||||
$"({boundaryResult.InboundCount} inbound, {boundaryResult.OutboundCount} outbound)"));
|
||||
|
||||
// Step 7: Combine all results into final semantic entrypoint
|
||||
var semanticEntrypoint = BuildFinalResult(
|
||||
context,
|
||||
adapterResult,
|
||||
mergedCapabilities,
|
||||
threatResult,
|
||||
boundaryResult,
|
||||
capabilityResult);
|
||||
|
||||
return SemanticAnalysisResult.Successful(semanticEntrypoint);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
diagnostics.Add(SemanticDiagnostic.Error("ERR-001", $"Analysis failed: {ex.Message}"));
|
||||
return SemanticAnalysisResult.Failed(diagnostics.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs quick analysis returning only intent and capabilities.
|
||||
/// </summary>
|
||||
public async Task<QuickSemanticResult> AnalyzeQuickAsync(
|
||||
SemanticAnalysisContext context,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var capabilityResult = _capabilityDetector.Detect(context);
|
||||
var adapter = FindAdapter(context);
|
||||
|
||||
if (adapter is null)
|
||||
{
|
||||
return new QuickSemanticResult
|
||||
{
|
||||
Intent = ApplicationIntent.Unknown,
|
||||
Capabilities = capabilityResult.Capabilities,
|
||||
Confidence = capabilityResult.Confidence,
|
||||
Language = context.PrimaryLanguage
|
||||
};
|
||||
}
|
||||
|
||||
var adapterResult = await adapter.AnalyzeAsync(context, cancellationToken);
|
||||
|
||||
return new QuickSemanticResult
|
||||
{
|
||||
Intent = adapterResult.Intent,
|
||||
Capabilities = adapterResult.Capabilities | capabilityResult.Capabilities,
|
||||
Confidence = adapterResult.Confidence,
|
||||
Language = adapterResult.Language,
|
||||
Framework = adapterResult.Framework
|
||||
};
|
||||
}
|
||||
|
||||
private ISemanticEntrypointAnalyzer? FindAdapter(SemanticAnalysisContext context)
|
||||
{
|
||||
var language = context.PrimaryLanguage?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
// Try to infer from detected languages
|
||||
language = context.DetectedLanguages.FirstOrDefault()?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(language))
|
||||
return null;
|
||||
|
||||
return _adapters.FirstOrDefault(a =>
|
||||
a.SupportedLanguages.Any(l =>
|
||||
l.Equals(language, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private SemanticAnalysisResult CreatePartialResult(
|
||||
SemanticAnalysisContext context,
|
||||
CapabilityDetectionResult capabilityResult,
|
||||
List<SemanticDiagnostic> diagnostics)
|
||||
{
|
||||
var partial = new PartialSemanticResult
|
||||
{
|
||||
Intent = null,
|
||||
Capabilities = capabilityResult.Capabilities,
|
||||
Confidence = capabilityResult.Confidence,
|
||||
IncompleteReason = "No matching language adapter found"
|
||||
};
|
||||
|
||||
return SemanticAnalysisResult.Partial(partial, diagnostics.ToArray());
|
||||
}
|
||||
|
||||
private SemanticEntrypoint BuildFinalResult(
|
||||
SemanticAnalysisContext context,
|
||||
SemanticEntrypoint adapterResult,
|
||||
CapabilityClass mergedCapabilities,
|
||||
ThreatInferenceResult threatResult,
|
||||
DataBoundaryMappingResult boundaryResult,
|
||||
CapabilityDetectionResult capabilityResult)
|
||||
{
|
||||
// Combine confidence from all sources
|
||||
var combinedConfidence = SemanticConfidence.Combine(new[]
|
||||
{
|
||||
adapterResult.Confidence,
|
||||
capabilityResult.Confidence,
|
||||
threatResult.Confidence,
|
||||
boundaryResult.Confidence
|
||||
});
|
||||
|
||||
// Build metadata
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["risk_score"] = threatResult.OverallRiskScore.ToString("F3"),
|
||||
["capability_count"] = CountCapabilities(mergedCapabilities).ToString(),
|
||||
["threat_count"] = threatResult.ThreatVectors.Length.ToString(),
|
||||
["boundary_count"] = boundaryResult.Boundaries.Length.ToString(),
|
||||
["security_sensitive_boundaries"] = boundaryResult.SecuritySensitiveCount.ToString()
|
||||
};
|
||||
|
||||
if (context.ScanId is not null)
|
||||
metadata["scan_id"] = context.ScanId;
|
||||
|
||||
return new SemanticEntrypoint
|
||||
{
|
||||
Id = adapterResult.Id,
|
||||
Specification = context.Specification,
|
||||
Intent = adapterResult.Intent,
|
||||
Capabilities = mergedCapabilities,
|
||||
AttackSurface = threatResult.ThreatVectors,
|
||||
DataBoundaries = boundaryResult.Boundaries,
|
||||
Confidence = combinedConfidence,
|
||||
Language = adapterResult.Language,
|
||||
Framework = adapterResult.Framework,
|
||||
FrameworkVersion = adapterResult.FrameworkVersion,
|
||||
RuntimeVersion = adapterResult.RuntimeVersion,
|
||||
Metadata = metadata.ToImmutableDictionary(),
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountCapabilities(CapabilityClass caps)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (CapabilityClass flag in Enum.GetValues<CapabilityClass>())
|
||||
{
|
||||
if (flag != CapabilityClass.None && !IsCompositeFlag(flag) && caps.HasFlag(flag))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static bool IsCompositeFlag(CapabilityClass flag)
|
||||
{
|
||||
// Composite flags have multiple bits set
|
||||
var val = (long)flag;
|
||||
return val != 0 && (val & (val - 1)) != 0;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ISemanticEntrypointAnalyzer> CreateDefaultAdapters()
|
||||
{
|
||||
return new ISemanticEntrypointAnalyzer[]
|
||||
{
|
||||
new PythonSemanticAdapter(),
|
||||
new JavaSemanticAdapter(),
|
||||
new NodeSemanticAdapter(),
|
||||
new DotNetSemanticAdapter(),
|
||||
new GoSemanticAdapter(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick semantic analysis result with just intent and capabilities.
|
||||
/// </summary>
|
||||
public sealed record QuickSemanticResult
|
||||
{
|
||||
public required ApplicationIntent Intent { get; init; }
|
||||
public required CapabilityClass Capabilities { get; init; }
|
||||
public required SemanticConfidence Confidence { get; init; }
|
||||
public string? Language { get; init; }
|
||||
public string? Framework { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for semantic orchestrator.
|
||||
/// </summary>
|
||||
public static class SemanticEntrypointOrchestratorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a context from an entry trace result and container metadata.
|
||||
/// </summary>
|
||||
public static SemanticAnalysisContext CreateContext(
|
||||
this SemanticEntrypointOrchestrator _,
|
||||
EntryTraceResult entryTraceResult,
|
||||
IRootFileSystem fileSystem,
|
||||
ContainerMetadata? containerMetadata = null)
|
||||
{
|
||||
var metadata = containerMetadata ?? ContainerMetadata.Empty;
|
||||
|
||||
// Build specification from trace result and container metadata
|
||||
var spec = new EntrypointSpecification
|
||||
{
|
||||
Entrypoint = ExtractEntrypoint(entryTraceResult),
|
||||
Cmd = ExtractCmd(entryTraceResult),
|
||||
WorkingDirectory = ExtractWorkingDirectory(entryTraceResult),
|
||||
User = ExtractUser(entryTraceResult),
|
||||
Shell = metadata.Shell,
|
||||
Environment = metadata.Environment?.ToImmutableDictionary(),
|
||||
ExposedPorts = metadata.ExposedPorts,
|
||||
Volumes = metadata.Volumes,
|
||||
Labels = metadata.Labels?.ToImmutableDictionary(),
|
||||
ImageDigest = entryTraceResult.ImageDigest,
|
||||
ImageReference = metadata.ImageReference
|
||||
};
|
||||
|
||||
return new SemanticAnalysisContext
|
||||
{
|
||||
Specification = spec,
|
||||
EntryTraceResult = entryTraceResult,
|
||||
FileSystem = fileSystem,
|
||||
PrimaryLanguage = InferPrimaryLanguage(entryTraceResult),
|
||||
DetectedLanguages = InferDetectedLanguages(entryTraceResult),
|
||||
ManifestPaths = metadata.ManifestPaths ?? new Dictionary<string, string>(),
|
||||
Dependencies = metadata.Dependencies ?? new Dictionary<string, IReadOnlyList<string>>(),
|
||||
ImageDigest = entryTraceResult.ImageDigest,
|
||||
ScanId = entryTraceResult.ScanId
|
||||
};
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractEntrypoint(EntryTraceResult result)
|
||||
{
|
||||
// Extract from first plan if available
|
||||
var plan = result.Graph.Plans.FirstOrDefault();
|
||||
return plan?.Command ?? ImmutableArray<string>.Empty;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> ExtractCmd(EntryTraceResult result)
|
||||
{
|
||||
// CMD is typically the arguments after entrypoint
|
||||
var plan = result.Graph.Plans.FirstOrDefault();
|
||||
if (plan is null || plan.Command.Length <= 1)
|
||||
return ImmutableArray<string>.Empty;
|
||||
|
||||
return plan.Command.Skip(1).ToImmutableArray();
|
||||
}
|
||||
|
||||
private static string? ExtractWorkingDirectory(EntryTraceResult result)
|
||||
{
|
||||
var plan = result.Graph.Plans.FirstOrDefault();
|
||||
return plan?.WorkingDirectory;
|
||||
}
|
||||
|
||||
private static string? ExtractUser(EntryTraceResult result)
|
||||
{
|
||||
var plan = result.Graph.Plans.FirstOrDefault();
|
||||
return plan?.User;
|
||||
}
|
||||
|
||||
private static string? InferPrimaryLanguage(EntryTraceResult result)
|
||||
{
|
||||
// Infer from terminal runtime or interpreter nodes
|
||||
var terminal = result.Graph.Terminals.FirstOrDefault();
|
||||
if (terminal?.Runtime is not null)
|
||||
{
|
||||
return terminal.Runtime.ToLowerInvariant() switch
|
||||
{
|
||||
var r when r.Contains("python") => "python",
|
||||
var r when r.Contains("node") => "node",
|
||||
var r when r.Contains("java") => "java",
|
||||
var r when r.Contains("dotnet") || r.Contains(".net") => "dotnet",
|
||||
var r when r.Contains("go") => "go",
|
||||
_ => terminal.Runtime
|
||||
};
|
||||
}
|
||||
|
||||
// Check interpreter nodes
|
||||
var interpreterNode = result.Graph.Nodes.FirstOrDefault(n => n.Kind == EntryTraceNodeKind.Interpreter);
|
||||
return interpreterNode?.InterpreterKind switch
|
||||
{
|
||||
EntryTraceInterpreterKind.Python => "python",
|
||||
EntryTraceInterpreterKind.Node => "node",
|
||||
EntryTraceInterpreterKind.Java => "java",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> InferDetectedLanguages(EntryTraceResult result)
|
||||
{
|
||||
var languages = new HashSet<string>();
|
||||
|
||||
foreach (var terminal in result.Graph.Terminals)
|
||||
{
|
||||
if (terminal.Runtime is not null)
|
||||
{
|
||||
var lang = terminal.Runtime.ToLowerInvariant() switch
|
||||
{
|
||||
var r when r.Contains("python") => "python",
|
||||
var r when r.Contains("node") => "node",
|
||||
var r when r.Contains("java") => "java",
|
||||
var r when r.Contains("dotnet") => "dotnet",
|
||||
var r when r.Contains("go") => "go",
|
||||
var r when r.Contains("ruby") => "ruby",
|
||||
var r when r.Contains("rust") => "rust",
|
||||
_ => null
|
||||
};
|
||||
if (lang is not null) languages.Add(lang);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in result.Graph.Nodes)
|
||||
{
|
||||
var lang = node.InterpreterKind switch
|
||||
{
|
||||
EntryTraceInterpreterKind.Python => "python",
|
||||
EntryTraceInterpreterKind.Node => "node",
|
||||
EntryTraceInterpreterKind.Java => "java",
|
||||
_ => null
|
||||
};
|
||||
if (lang is not null) languages.Add(lang);
|
||||
}
|
||||
|
||||
return languages.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container metadata not present in EntryTraceResult.
|
||||
/// </summary>
|
||||
public sealed record ContainerMetadata
|
||||
{
|
||||
public string? Shell { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? Environment { get; init; }
|
||||
public ImmutableArray<int> ExposedPorts { get; init; } = ImmutableArray<int>.Empty;
|
||||
public ImmutableArray<string> Volumes { get; init; } = ImmutableArray<string>.Empty;
|
||||
public IReadOnlyDictionary<string, string>? Labels { get; init; }
|
||||
public string? ImageReference { get; init; }
|
||||
public IReadOnlyDictionary<string, string>? ManifestPaths { get; init; }
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>>? Dependencies { get; init; }
|
||||
|
||||
public static ContainerMetadata Empty => new();
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Semantic;
|
||||
|
||||
/// <summary>
|
||||
/// Types of security threat vectors inferred from entrypoint analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 4).
|
||||
/// </remarks>
|
||||
public enum ThreatVectorType
|
||||
{
|
||||
/// <summary>Server-Side Request Forgery.</summary>
|
||||
Ssrf = 1,
|
||||
/// <summary>SQL Injection.</summary>
|
||||
SqlInjection = 2,
|
||||
/// <summary>Cross-Site Scripting.</summary>
|
||||
Xss = 3,
|
||||
/// <summary>Remote Code Execution.</summary>
|
||||
Rce = 4,
|
||||
/// <summary>Path Traversal.</summary>
|
||||
PathTraversal = 5,
|
||||
/// <summary>Insecure Deserialization.</summary>
|
||||
InsecureDeserialization = 6,
|
||||
/// <summary>Template Injection.</summary>
|
||||
TemplateInjection = 7,
|
||||
/// <summary>Authentication Bypass.</summary>
|
||||
AuthenticationBypass = 8,
|
||||
/// <summary>Authorization Bypass.</summary>
|
||||
AuthorizationBypass = 9,
|
||||
/// <summary>Information Disclosure.</summary>
|
||||
InformationDisclosure = 10,
|
||||
/// <summary>Denial of Service.</summary>
|
||||
DenialOfService = 11,
|
||||
/// <summary>Command Injection.</summary>
|
||||
CommandInjection = 12,
|
||||
/// <summary>LDAP Injection.</summary>
|
||||
LdapInjection = 13,
|
||||
/// <summary>XML External Entity.</summary>
|
||||
XxeInjection = 14,
|
||||
/// <summary>Open Redirect.</summary>
|
||||
OpenRedirect = 15,
|
||||
/// <summary>Insecure Direct Object Reference.</summary>
|
||||
Idor = 16,
|
||||
/// <summary>Cross-Site Request Forgery.</summary>
|
||||
Csrf = 17,
|
||||
/// <summary>Cryptographic Weakness.</summary>
|
||||
CryptoWeakness = 18,
|
||||
/// <summary>Container Escape.</summary>
|
||||
ContainerEscape = 19,
|
||||
/// <summary>Privilege Escalation.</summary>
|
||||
PrivilegeEscalation = 20,
|
||||
/// <summary>Mass Assignment.</summary>
|
||||
MassAssignment = 21,
|
||||
/// <summary>Log Injection.</summary>
|
||||
LogInjection = 22,
|
||||
/// <summary>Header Injection.</summary>
|
||||
HeaderInjection = 23,
|
||||
/// <summary>Regex Denial of Service.</summary>
|
||||
ReDoS = 24,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inferred threat vector with confidence and evidence.
|
||||
/// </summary>
|
||||
public sealed record ThreatVector
|
||||
{
|
||||
/// <summary>The type of threat vector.</summary>
|
||||
public required ThreatVectorType Type { get; init; }
|
||||
|
||||
/// <summary>Confidence in the inference (0.0-1.0).</summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>Capabilities that contributed to this inference.</summary>
|
||||
public required CapabilityClass ContributingCapabilities { get; init; }
|
||||
|
||||
/// <summary>Evidence strings explaining why this was inferred.</summary>
|
||||
public required ImmutableArray<string> Evidence { get; init; }
|
||||
|
||||
/// <summary>Entry paths where this threat vector is reachable.</summary>
|
||||
public ImmutableArray<string> EntryPaths { get; init; } = ImmutableArray<string>.Empty;
|
||||
|
||||
/// <summary>Additional metadata.</summary>
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ThreatVectorType.
|
||||
/// </summary>
|
||||
public static class ThreatVectorTypeExtensions
|
||||
{
|
||||
/// <summary>Gets the OWASP Top 10 category.</summary>
|
||||
public static string? GetOwaspCategory(this ThreatVectorType type) => type switch
|
||||
{
|
||||
ThreatVectorType.SqlInjection => "A03:2021-Injection",
|
||||
ThreatVectorType.CommandInjection => "A03:2021-Injection",
|
||||
ThreatVectorType.LdapInjection => "A03:2021-Injection",
|
||||
ThreatVectorType.XxeInjection => "A03:2021-Injection",
|
||||
ThreatVectorType.TemplateInjection => "A03:2021-Injection",
|
||||
ThreatVectorType.Xss => "A03:2021-Injection",
|
||||
ThreatVectorType.AuthenticationBypass => "A07:2021-Identification and Authentication Failures",
|
||||
ThreatVectorType.AuthorizationBypass => "A01:2021-Broken Access Control",
|
||||
ThreatVectorType.Idor => "A01:2021-Broken Access Control",
|
||||
ThreatVectorType.PathTraversal => "A01:2021-Broken Access Control",
|
||||
ThreatVectorType.InsecureDeserialization => "A08:2021-Software and Data Integrity Failures",
|
||||
ThreatVectorType.CryptoWeakness => "A02:2021-Cryptographic Failures",
|
||||
ThreatVectorType.InformationDisclosure => "A02:2021-Cryptographic Failures",
|
||||
ThreatVectorType.Ssrf => "A10:2021-Server-Side Request Forgery",
|
||||
ThreatVectorType.Csrf => "A01:2021-Broken Access Control",
|
||||
ThreatVectorType.Rce => "A03:2021-Injection",
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>Gets the CWE ID.</summary>
|
||||
public static int? GetCweId(this ThreatVectorType type) => type switch
|
||||
{
|
||||
ThreatVectorType.Ssrf => 918,
|
||||
ThreatVectorType.SqlInjection => 89,
|
||||
ThreatVectorType.Xss => 79,
|
||||
ThreatVectorType.Rce => 94,
|
||||
ThreatVectorType.PathTraversal => 22,
|
||||
ThreatVectorType.InsecureDeserialization => 502,
|
||||
ThreatVectorType.TemplateInjection => 1336,
|
||||
ThreatVectorType.AuthenticationBypass => 287,
|
||||
ThreatVectorType.AuthorizationBypass => 862,
|
||||
ThreatVectorType.InformationDisclosure => 200,
|
||||
ThreatVectorType.DenialOfService => 400,
|
||||
ThreatVectorType.CommandInjection => 78,
|
||||
ThreatVectorType.LdapInjection => 90,
|
||||
ThreatVectorType.XxeInjection => 611,
|
||||
ThreatVectorType.OpenRedirect => 601,
|
||||
ThreatVectorType.Idor => 639,
|
||||
ThreatVectorType.Csrf => 352,
|
||||
ThreatVectorType.CryptoWeakness => 327,
|
||||
ThreatVectorType.ContainerEscape => 1022,
|
||||
ThreatVectorType.PrivilegeEscalation => 269,
|
||||
ThreatVectorType.MassAssignment => 915,
|
||||
ThreatVectorType.LogInjection => 117,
|
||||
ThreatVectorType.HeaderInjection => 113,
|
||||
ThreatVectorType.ReDoS => 1333,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.EntryTrace.Diagnostics;
|
||||
using StellaOps.Scanner.EntryTrace.Runtime;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic.Adapters;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic.Analysis;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace;
|
||||
|
||||
@@ -29,4 +32,83 @@ public static class ServiceCollectionExtensions
|
||||
services.TryAddSingleton<IEntryTraceResultStore, NullEntryTraceResultStore>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds entry trace analyzer with integrated semantic analysis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Part of Sprint 0411 - Semantic Entrypoint Engine (Task 17).
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddSemanticEntryTraceAnalyzer(
|
||||
this IServiceCollection services,
|
||||
Action<EntryTraceAnalyzerOptions>? configure = null,
|
||||
Action<SemanticAnalysisOptions>? configureSemantic = null)
|
||||
{
|
||||
if (services is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
// Add base entry trace analyzer
|
||||
services.AddEntryTraceAnalyzer(configure);
|
||||
|
||||
// Add semantic analysis options
|
||||
services.AddOptions<SemanticAnalysisOptions>()
|
||||
.BindConfiguration(SemanticAnalysisOptions.SectionName);
|
||||
|
||||
if (configureSemantic is not null)
|
||||
{
|
||||
services.Configure(configureSemantic);
|
||||
}
|
||||
|
||||
// Register semantic analysis components
|
||||
services.TryAddSingleton<CapabilityDetector>();
|
||||
services.TryAddSingleton<ThreatVectorInferrer>();
|
||||
services.TryAddSingleton<DataBoundaryMapper>();
|
||||
|
||||
// Register language adapters
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISemanticEntrypointAnalyzer, PythonSemanticAdapter>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISemanticEntrypointAnalyzer, JavaSemanticAdapter>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISemanticEntrypointAnalyzer, NodeSemanticAdapter>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISemanticEntrypointAnalyzer, DotNetSemanticAdapter>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ISemanticEntrypointAnalyzer, GoSemanticAdapter>());
|
||||
|
||||
// Register orchestrator
|
||||
services.TryAddSingleton<SemanticEntrypointOrchestrator>(sp =>
|
||||
{
|
||||
var adapters = sp.GetServices<ISemanticEntrypointAnalyzer>().ToList();
|
||||
var capabilityDetector = sp.GetRequiredService<CapabilityDetector>();
|
||||
var threatInferrer = sp.GetRequiredService<ThreatVectorInferrer>();
|
||||
var boundaryMapper = sp.GetRequiredService<DataBoundaryMapper>();
|
||||
return new SemanticEntrypointOrchestrator(adapters, capabilityDetector, threatInferrer, boundaryMapper);
|
||||
});
|
||||
|
||||
// Register semantic entry trace analyzer
|
||||
services.TryAddSingleton<ISemanticEntryTraceAnalyzer, SemanticEntryTraceAnalyzer>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for semantic analysis behavior.
|
||||
/// </summary>
|
||||
public sealed class SemanticAnalysisOptions
|
||||
{
|
||||
public const string SectionName = "Scanner:EntryTrace:Semantic";
|
||||
|
||||
/// <summary>Whether semantic analysis is enabled.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Minimum confidence threshold for threat vectors (0.0-1.0).</summary>
|
||||
public double ThreatConfidenceThreshold { get; set; } = 0.3;
|
||||
|
||||
/// <summary>Maximum number of threat vectors to emit per entrypoint.</summary>
|
||||
public int MaxThreatVectors { get; set; } = 50;
|
||||
|
||||
/// <summary>Whether to include low-confidence capabilities.</summary>
|
||||
public bool IncludeLowConfidenceCapabilities { get; set; } = false;
|
||||
|
||||
/// <summary>Languages to include in semantic analysis (empty = all).</summary>
|
||||
public IReadOnlyList<string> EnabledLanguages { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user