up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

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

View File

@@ -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]}";
}
}

View File

@@ -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]}";
}
}

View File

@@ -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]}";
}
}

View File

@@ -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]}";
}
}

View File

@@ -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]}";
}
}

View File

@@ -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,
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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
};
}

View File

@@ -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,
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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")
};
}
}

View File

@@ -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();
}

View File

@@ -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
};
}

View File

@@ -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>();
}