Files
git.stella-ops.org/docs/cli/architecture.md
master fcb5ffe25d feat(scanner): Complete PoE implementation with Windows compatibility fix
- Fix namespace conflicts (Subgraph → PoESubgraph)
- Add hash sanitization for Windows filesystem (colon → underscore)
- Update all test mocks to use It.IsAny<>()
- Add direct orchestrator unit tests
- All 8 PoE tests now passing (100% success rate)
- Complete SPRINT_3500_0001_0001 documentation

Fixes compilation errors and Windows filesystem compatibility issues.
Tests: 8/8 passing
Files: 8 modified, 1 new test, 1 completion report

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 14:52:08 +02:00

23 KiB

stella CLI - Plugin Architecture

Sprint: SPRINT_4100_0006_0006 - CLI Documentation Overhaul

Overview

The stella CLI is built with a plugin architecture that enables conditional compilation of regional cryptographic providers (GOST, eIDAS, SM) while maintaining a unified command interface. This design supports compliance with export control regulations and cryptographic standards across different jurisdictions.

Key Design Goals:

  1. Conditional Compilation: Include only authorized crypto providers per distribution
  2. Plugin Isolation: Crypto providers as self-contained, testable modules
  3. Dependency Injection: Runtime service resolution for providers
  4. Configuration-driven: Profile-based provider selection
  5. Extensibility: Easy addition of new providers without core CLI changes

Architecture Layers

┌─────────────────────────────────────────────────────────────┐
│                       stella CLI                             │
├─────────────────────────────────────────────────────────────┤
│  Command Groups                                              │
│  ├─ scan, aoc, symbols, crypto, admin, ...                  │
│  └─ System.CommandLine 2.0 routing                          │
├─────────────────────────────────────────────────────────────┤
│  Plugin System                                               │
│  ├─ ICryptoProvider interface                               │
│  ├─ Plugin discovery (build-time + runtime)                 │
│  └─ DependencyInjection (Microsoft.Extensions.DI)           │
├─────────────────────────────────────────────────────────────┤
│  Crypto Plugins (Conditional)                                │
│  ├─ Default (.NET Crypto, BouncyCastle)        [ALL]        │
│  ├─ GOST (CryptoPro, OpenSSL-GOST, PKCS#11)    [RUSSIA]     │
│  ├─ eIDAS (TSP Client, Local Signer)           [EU]         │
│  └─ SM (GmSSL, SM Remote CSP)                  [CHINA]      │
├─────────────────────────────────────────────────────────────┤
│  Backend Integration                                         │
│  ├─ Authority (OAuth2 + DPoP)                               │
│  ├─ Scanner, Concelier, Policy, ...                         │
│  └─ HTTP clients with retry policies                        │
└─────────────────────────────────────────────────────────────┘

Build-time Plugin Selection

Conditional Compilation Workflow

graph TD
    A[MSBuild Start] --> B{Check Build Flags}
    B -->|StellaOpsEnableGOST=true| C[Include GOST Plugin]
    B -->|StellaOpsEnableEIDAS=true| D[Include eIDAS Plugin]
    B -->|StellaOpsEnableSM=true| E[Include SM Plugin]
    B -->|No flags| F[Include Default Only]

    C --> G[Set STELLAOPS_ENABLE_GOST]
    D --> H[Set STELLAOPS_ENABLE_EIDAS]
    E --> I[Set STELLAOPS_ENABLE_SM]

    G --> J[Compile with Plugin]
    H --> J
    I --> J
    F --> J

    J --> K[Link Plugin Assembly]
    K --> L[Final Binary]

Project Structure

src/Cli/
├── StellaOps.Cli/
│   ├── Program.cs                          # Entry point, DI setup
│   ├── Commands/
│   │   ├── CommandFactory.cs               # Command routing
│   │   ├── Crypto/CryptoCommandGroup.cs    # Crypto commands
│   │   ├── Admin/AdminCommandGroup.cs      # Admin commands
│   │   └── ...
│   └── StellaOps.Cli.csproj                # Conditional <ProjectReference>
│
├── StellaOps.Cli.Crypto/
│   ├── ICryptoProvider.cs                  # Plugin interface
│   ├── ICryptoProviderDiagnostics.cs       # Diagnostics interface
│   └── Models/                             # Shared models
│
├── StellaOps.Cli.Crypto.Default/           # Always included
│   ├── DotNetCryptoProvider.cs             # .NET crypto
│   ├── BouncyCastleCryptoProvider.cs       # BouncyCastle
│   └── ServiceCollectionExtensions.cs      # DI registration
│
├── StellaOps.Cli.Crypto.Gost/              # Conditional (Russia)
│   ├── GostCryptoProvider.cs               # GOST implementation
│   ├── CryptoProAdapter.cs                 # CryptoPro CSP adapter
│   ├── OpenSslGostAdapter.cs               # OpenSSL-GOST adapter
│   └── ServiceCollectionExtensions.cs
│
├── StellaOps.Cli.Crypto.Eidas/             # Conditional (EU)
│   ├── EidasCryptoProvider.cs              # eIDAS implementation
│   ├── TspClientAdapter.cs                 # TSP remote signing
│   └── ServiceCollectionExtensions.cs
│
└── StellaOps.Cli.Crypto.Sm/                # Conditional (China)
    ├── SmCryptoProvider.cs                 # SM implementation
    ├── GmSslAdapter.cs                     # GmSSL adapter
    └── ServiceCollectionExtensions.cs

StellaOps.Cli.csproj (Conditional References)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <!-- Always include default crypto -->
  <ItemGroup>
    <ProjectReference Include="..\StellaOps.Cli.Crypto\StellaOps.Cli.Crypto.csproj" />
    <ProjectReference Include="..\StellaOps.Cli.Crypto.Default\StellaOps.Cli.Crypto.Default.csproj" />
  </ItemGroup>

  <!-- GOST plugin (Russia distribution) -->
  <ItemGroup Condition="'$(StellaOpsEnableGOST)' == 'true'">
    <ProjectReference Include="..\StellaOps.Cli.Crypto.Gost\StellaOps.Cli.Crypto.Gost.csproj" />
    <DefineConstants>$(DefineConstants);STELLAOPS_ENABLE_GOST</DefineConstants>
  </ItemGroup>

  <!-- eIDAS plugin (EU distribution) -->
  <ItemGroup Condition="'$(StellaOpsEnableEIDAS)' == 'true'">
    <ProjectReference Include="..\StellaOps.Cli.Crypto.Eidas\StellaOps.Cli.Crypto.Eidas.csproj" />
    <DefineConstants>$(DefineConstants);STELLAOPS_ENABLE_EIDAS</DefineConstants>
  </ItemGroup>

  <!-- SM plugin (China distribution) -->
  <ItemGroup Condition="'$(StellaOpsEnableSM)' == 'true'">
    <ProjectReference Include="..\StellaOps.Cli.Crypto.Sm\StellaOps.Cli.Crypto.Sm.csproj" />
    <DefineConstants>$(DefineConstants);STELLAOPS_ENABLE_SM</DefineConstants>
  </ItemGroup>
</Project>

Build Commands

# International distribution (default, no flags)
dotnet publish src/Cli/StellaOps.Cli --configuration Release --runtime linux-x64

# Russia distribution (GOST enabled)
dotnet publish src/Cli/StellaOps.Cli \
  --configuration Release \
  --runtime linux-x64 \
  -p:StellaOpsEnableGOST=true

# EU distribution (eIDAS enabled)
dotnet publish src/Cli/StellaOps.Cli \
  --configuration Release \
  --runtime linux-x64 \
  -p:StellaOpsEnableEIDAS=true

# China distribution (SM enabled)
dotnet publish src/Cli/StellaOps.Cli \
  --configuration Release \
  --runtime linux-x64 \
  -p:StellaOpsEnableSM=true

Runtime Plugin Discovery

Program.cs - DI Registration

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cli.Crypto;
using StellaOps.Cli.Crypto.Default;

#if STELLAOPS_ENABLE_GOST
using StellaOps.Cli.Crypto.Gost;
#endif

#if STELLAOPS_ENABLE_EIDAS
using StellaOps.Cli.Crypto.Eidas;
#endif

#if STELLAOPS_ENABLE_SM
using StellaOps.Cli.Crypto.Sm;
#endif

namespace StellaOps.Cli;

public class Program
{
    public static async Task<int> Main(string[] args)
    {
        // Build configuration
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true)
            .AddYamlFile("appsettings.yaml", optional: true)
            .AddYamlFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".stellaops", "config.yaml"), optional: true)
            .AddEnvironmentVariables("STELLAOPS_")
            .Build();

        // Setup DI container
        var services = new ServiceCollection();

        // Register configuration
        services.AddSingleton<IConfiguration>(configuration);

        // Register HTTP clients
        services.AddHttpClient("StellaOpsBackend", client =>
        {
            var baseUrl = configuration["StellaOps:Backend:BaseUrl"];
            if (!string.IsNullOrEmpty(baseUrl))
                client.BaseAddress = new Uri(baseUrl);
        });

        // Register default crypto providers (always available)
        services.AddDefaultCryptoProviders(configuration);

        // Register regional crypto providers (conditional compilation)
#if STELLAOPS_ENABLE_GOST
        services.AddGostCryptoProviders(configuration);
#endif

#if STELLAOPS_ENABLE_EIDAS
        services.AddEidasCryptoProviders(configuration);
#endif

#if STELLAOPS_ENABLE_SM
        services.AddSmCryptoProviders(configuration);
#endif

        // Build service provider
        var serviceProvider = services.BuildServiceProvider();

        // Create root command and run
        var rootCommand = CommandFactory.CreateRootCommand(serviceProvider);
        return await rootCommand.InvokeAsync(args);
    }
}

Plugin Discovery Flow

sequenceDiagram
    participant Build as MSBuild
    participant CLI as stella CLI
    participant DI as DI Container
    participant Plugin as Crypto Plugin
    participant User as User Command

    Build->>Build: Check StellaOpsEnableGOST=true
    Build->>Build: Include GOST plugin <ProjectReference>
    Build->>Build: Set DefineConstants=STELLAOPS_ENABLE_GOST
    Build->>CLI: Compile with GOST plugin

    User->>CLI: stella crypto sign --provider gost
    CLI->>CLI: Program.cs startup
    CLI->>CLI: Check #if STELLAOPS_ENABLE_GOST
    CLI->>DI: services.AddGostCryptoProviders()
    DI->>Plugin: Register GostCryptoProvider as ICryptoProvider
    Plugin->>DI: Provider registered

    CLI->>DI: Resolve ICryptoProvider (name="gost")
    DI->>Plugin: Return GostCryptoProvider instance
    Plugin->>CLI: Execute sign operation
    CLI->>User: Signature created

Plugin Interfaces

ICryptoProvider

The core interface all crypto providers must implement:

namespace StellaOps.Cli.Crypto;

public interface ICryptoProvider
{
    /// <summary>
    /// Unique provider name (e.g., "gost", "eidas", "sm")
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Supported algorithms (e.g., "GOST12-256", "ECDSA-P256")
    /// </summary>
    string[] SupportedAlgorithms { get; }

    /// <summary>
    /// Sign data with specified algorithm and key
    /// </summary>
    Task<byte[]> SignAsync(
        byte[] data,
        string algorithm,
        CryptoKeyReference keyRef,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// Verify signature
    /// </summary>
    Task<bool> VerifyAsync(
        byte[] data,
        byte[] signature,
        string algorithm,
        CryptoKeyReference keyRef,
        CancellationToken cancellationToken = default);

    /// <summary>
    /// List available keys
    /// </summary>
    Task<IReadOnlyList<CryptoKeyInfo>> ListKeysAsync(
        CancellationToken cancellationToken = default);
}

ICryptoProviderDiagnostics

Optional interface for provider diagnostics:

namespace StellaOps.Cli.Crypto;

public interface ICryptoProviderDiagnostics
{
    /// <summary>
    /// Run provider self-test
    /// </summary>
    Task<ProviderHealthCheck> HealthCheckAsync(CancellationToken cancellationToken = default);

    /// <summary>
    /// Get provider version and capabilities
    /// </summary>
    ProviderInfo GetInfo();
}

public sealed record ProviderHealthCheck
{
    public required string ProviderName { get; init; }
    public required bool IsHealthy { get; init; }
    public required string[] Checks { get; init; }
    public string? ErrorMessage { get; init; }
}

public sealed record ProviderInfo
{
    public required string Name { get; init; }
    public required string Version { get; init; }
    public required string[] Capabilities { get; init; }
    public required string[] SupportedAlgorithms { get; init; }
}

CryptoKeyReference

Represents a reference to a cryptographic key:

namespace StellaOps.Cli.Crypto;

public sealed record CryptoKeyReference
{
    /// <summary>
    /// Key identifier (e.g., "prod-key-2024", file path, HSM slot)
    /// </summary>
    public required string KeyId { get; init; }

    /// <summary>
    /// Key source: "file", "hsm", "kms", "csp"
    /// </summary>
    public required string Source { get; init; }

    /// <summary>
    /// Additional parameters (e.g., HSM PIN, KMS region)
    /// </summary>
    public IReadOnlyDictionary<string, string>? Parameters { get; init; }
}

Configuration

Profile-based Provider Selection

StellaOps:
  Crypto:
    # Default provider (when --provider not specified)
    DefaultProvider: "default"

    # Crypto profiles for easy switching
    Profiles:
      - name: "default-signing"
        provider: "default"
        algorithm: "ECDSA-P256"
        keyId: "default-key"

      - name: "gost-signing"
        provider: "gost"
        algorithm: "GOST12-256"
        keyId: "gost-key-2024"

      - name: "eidas-qes"
        provider: "eidas"
        algorithm: "ECDSA-P256-QES"
        keyId: "eidas-qes-key"

    # Provider-specific configuration
    Providers:
      Gost:
        CryptoProCsp:
          Enabled: true
          ContainerName: "StellaOps-GOST-2024"

        OpenSslGost:
          Enabled: false
          EnginePath: "/usr/lib/engines/gost.so"

      Eidas:
        TspClient:
          Enabled: true
          TspUrl: "https://tsp.example.eu/api/v1/sign"
          ApiKey: "${EIDAS_TSP_API_KEY}"

      Sm:
        GmSsl:
          Enabled: true
          LibraryPath: "/usr/lib/libgmssl.so"

Usage with Profiles

# Use default profile
stella crypto sign --file document.pdf

# Use specific profile
stella crypto sign --profile gost-signing --file document.pdf

# Override provider explicitly
stella crypto sign --provider gost --algorithm GOST12-256 --key-id key1 --file document.pdf

Distribution Matrix

Distribution Default GOST eIDAS SM
stella-international
stella-russia
stella-eu
stella-china

Verification:

# Check available providers
stella crypto providers

# Output (International):
# Available Crypto Providers:
# - default (.NET Crypto, BouncyCastle)

# Output (Russia):
# Available Crypto Providers:
# - default (.NET Crypto, BouncyCastle)
# - gost (GOST R 34.10-2012, GOST R 34.11-2012)

Creating Custom Plugins

1. Create Plugin Project

dotnet new classlib -n StellaOps.Cli.Crypto.MyCustom
cd StellaOps.Cli.Crypto.MyCustom

# Add reference to interface project
dotnet add reference ../StellaOps.Cli.Crypto/StellaOps.Cli.Crypto.csproj

2. Implement ICryptoProvider

using StellaOps.Cli.Crypto;

namespace StellaOps.Cli.Crypto.MyCustom;

public class MyCustomCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
{
    private readonly MyCustomCryptoOptions _options;

    public MyCustomCryptoProvider(IOptions<MyCustomCryptoOptions> options)
    {
        _options = options.Value;
    }

    public string Name => "mycustom";

    public string[] SupportedAlgorithms => new[] { "MYCUSTOM-ALG1", "MYCUSTOM-ALG2" };

    public async Task<byte[]> SignAsync(
        byte[] data,
        string algorithm,
        CryptoKeyReference keyRef,
        CancellationToken cancellationToken = default)
    {
        // Implementation
        throw new NotImplementedException();
    }

    public async Task<bool> VerifyAsync(
        byte[] data,
        byte[] signature,
        string algorithm,
        CryptoKeyReference keyRef,
        CancellationToken cancellationToken = default)
    {
        // Implementation
        throw new NotImplementedException();
    }

    public async Task<IReadOnlyList<CryptoKeyInfo>> ListKeysAsync(
        CancellationToken cancellationToken = default)
    {
        // Implementation
        throw new NotImplementedException();
    }

    public async Task<ProviderHealthCheck> HealthCheckAsync(
        CancellationToken cancellationToken = default)
    {
        return new ProviderHealthCheck
        {
            ProviderName = Name,
            IsHealthy = true,
            Checks = new[] { "Library loaded", "Keys accessible" }
        };
    }

    public ProviderInfo GetInfo()
    {
        return new ProviderInfo
        {
            Name = Name,
            Version = "1.0.0",
            Capabilities = new[] { "sign", "verify" },
            SupportedAlgorithms = SupportedAlgorithms
        };
    }
}

3. Create DI Extension

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace StellaOps.Cli.Crypto.MyCustom;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyCustomCryptoProviders(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Register provider
        services.AddSingleton<ICryptoProvider, MyCustomCryptoProvider>();

        // Bind configuration
        services.Configure<MyCustomCryptoOptions>(
            configuration.GetSection("StellaOps:Crypto:Providers:MyCustom"));

        return services;
    }
}

4. Update StellaOps.Cli.csproj

<!-- MyCustom plugin (custom distribution) -->
<ItemGroup Condition="'$(StellaOpsEnableMyCustom)' == 'true'">
  <ProjectReference Include="..\StellaOps.Cli.Crypto.MyCustom\StellaOps.Cli.Crypto.MyCustom.csproj" />
  <DefineConstants>$(DefineConstants);STELLAOPS_ENABLE_MYCUSTOM</DefineConstants>
</ItemGroup>

5. Update Program.cs

#if STELLAOPS_ENABLE_MYCUSTOM
using StellaOps.Cli.Crypto.MyCustom;
#endif

// In Main():
#if STELLAOPS_ENABLE_MYCUSTOM
services.AddMyCustomCryptoProviders(configuration);
#endif

6. Build Custom Distribution

dotnet publish src/Cli/StellaOps.Cli \
  --configuration Release \
  --runtime linux-x64 \
  -p:StellaOpsEnableMyCustom=true

Command Routing

System.CommandLine 2.0 Integration

// CommandFactory.cs
using System.CommandLine;

public static class CommandFactory
{
    public static Command CreateRootCommand(IServiceProvider services)
    {
        var root = new Command("stella", "StellaOps unified CLI");

        // Add command groups
        root.Add(BuildScanCommand(services));
        root.Add(BuildCryptoCommand(services));
        root.Add(BuildAdminCommand(services));
        root.Add(BuildAuthCommand(services));
        // ... more commands

        return root;
    }

    private static Command BuildCryptoCommand(IServiceProvider services)
    {
        var crypto = new Command("crypto", "Cryptographic operations");

        // crypto providers
        var providers = new Command("providers", "List available crypto providers");
        providers.SetAction(async (parseResult, ct) =>
        {
            var cryptoProviders = services.GetServices<ICryptoProvider>();
            foreach (var provider in cryptoProviders)
            {
                Console.WriteLine($"- {provider.Name}: {string.Join(", ", provider.SupportedAlgorithms)}");
            }
            return 0;
        });
        crypto.Add(providers);

        // crypto sign
        var sign = new Command("sign", "Sign file");
        // ... add options and handler
        crypto.Add(sign);

        return crypto;
    }
}

Testing

Unit Tests

using StellaOps.Cli.Crypto;
using StellaOps.Cli.Crypto.Gost;
using Xunit;

public class GostCryptoProviderTests
{
    [Fact]
    public void Name_ReturnsGost()
    {
        var provider = new GostCryptoProvider(Options.Create(new GostCryptoOptions()));
        Assert.Equal("gost", provider.Name);
    }

    [Fact]
    public void SupportedAlgorithms_IncludesGost12_256()
    {
        var provider = new GostCryptoProvider(Options.Create(new GostCryptoOptions()));
        Assert.Contains("GOST12-256", provider.SupportedAlgorithms);
    }

    [Fact]
    public async Task SignAsync_ProducesSignature()
    {
        var provider = new GostCryptoProvider(Options.Create(new GostCryptoOptions()));
        var data = "test"u8.ToArray();
        var keyRef = new CryptoKeyReference { KeyId = "test-key", Source = "file" };

        var signature = await provider.SignAsync(data, "GOST12-256", keyRef);

        Assert.NotNull(signature);
        Assert.NotEmpty(signature);
    }
}

Integration Tests

using Microsoft.Extensions.DependencyInjection;
using Xunit;

public class CryptoPluginIntegrationTests
{
    [Fact]
    public void ServiceProvider_ResolvesAllProviders()
    {
        var services = new ServiceCollection();
        var configuration = new ConfigurationBuilder().Build();

        services.AddDefaultCryptoProviders(configuration);
#if STELLAOPS_ENABLE_GOST
        services.AddGostCryptoProviders(configuration);
#endif

        var serviceProvider = services.BuildServiceProvider();
        var providers = serviceProvider.GetServices<ICryptoProvider>().ToList();

        Assert.NotEmpty(providers);
        Assert.Contains(providers, p => p.Name == "default");
#if STELLAOPS_ENABLE_GOST
        Assert.Contains(providers, p => p.Name == "gost");
#endif
    }
}

Packaging

NuGet Package Structure

StellaOps.Cli (metapackage)
├── StellaOps.Cli.Crypto (interfaces)
├── StellaOps.Cli.Crypto.Default (always included)
├── StellaOps.Cli.Crypto.Gost (optional)
├── StellaOps.Cli.Crypto.Eidas (optional)
└── StellaOps.Cli.Crypto.Sm (optional)

Distribution Artifacts

releases/
├── stella-international-linux-x64.tar.gz
├── stella-russia-linux-x64.tar.gz
├── stella-eu-linux-x64.tar.gz
└── stella-china-linux-x64.tar.gz

Each artifact contains only the authorized crypto providers for that region.


See Also