581 lines
12 KiB
Markdown
581 lines
12 KiB
Markdown
# Plugin Development SDK
|
|
|
|
This guide covers the StellaOps Plugin SDK for developing custom plugins.
|
|
|
|
## Overview
|
|
|
|
The Plugin SDK provides:
|
|
|
|
- Base interfaces for all plugin types
|
|
- DI registration utilities
|
|
- Configuration binding helpers
|
|
- Version compatibility attributes
|
|
- Testing infrastructure
|
|
|
|
## Prerequisites
|
|
|
|
- .NET 10 SDK
|
|
- Understanding of dependency injection
|
|
- Familiarity with async/await patterns
|
|
|
|
## Core Interfaces
|
|
|
|
### IAvailabilityPlugin
|
|
|
|
Base interface for plugins with availability checks:
|
|
|
|
```csharp
|
|
namespace StellaOps.Plugin;
|
|
|
|
public interface IAvailabilityPlugin
|
|
{
|
|
/// <summary>
|
|
/// Checks whether the plugin is available in the current environment.
|
|
/// </summary>
|
|
bool IsAvailable(IServiceProvider services);
|
|
}
|
|
```
|
|
|
|
### IRouterTransportPlugin
|
|
|
|
Interface for router transport plugins:
|
|
|
|
```csharp
|
|
namespace StellaOps.Router.Common.Plugins;
|
|
|
|
public interface IRouterTransportPlugin
|
|
{
|
|
string TransportName { get; }
|
|
string DisplayName { get; }
|
|
bool IsAvailable(IServiceProvider services);
|
|
void Register(RouterTransportRegistrationContext context);
|
|
}
|
|
```
|
|
|
|
### IConcielierConnector
|
|
|
|
Interface for vulnerability data connectors:
|
|
|
|
```csharp
|
|
namespace StellaOps.Concelier.Core.Plugins;
|
|
|
|
public interface IConcielierConnector : IAvailabilityPlugin
|
|
{
|
|
string ConnectorId { get; }
|
|
string DisplayName { get; }
|
|
Task<IAsyncEnumerable<VulnerabilityRecord>> FetchAsync(
|
|
FetchOptions options,
|
|
CancellationToken cancellationToken);
|
|
}
|
|
```
|
|
|
|
### IScannerAnalyzerPlugin
|
|
|
|
Interface for language-specific scanners:
|
|
|
|
```csharp
|
|
namespace StellaOps.Scanner.Core.Plugins;
|
|
|
|
public interface IScannerAnalyzerPlugin : IAvailabilityPlugin
|
|
{
|
|
string AnalyzerId { get; }
|
|
string Language { get; }
|
|
IEnumerable<string> SupportedFilePatterns { get; }
|
|
Task<AnalysisResult> AnalyzeAsync(
|
|
AnalysisContext context,
|
|
CancellationToken cancellationToken);
|
|
}
|
|
```
|
|
|
|
## Creating a Plugin
|
|
|
|
### 1. Create Project
|
|
|
|
```bash
|
|
mkdir MyCompany.StellaOps.Plugin.MyPlugin
|
|
cd MyCompany.StellaOps.Plugin.MyPlugin
|
|
dotnet new classlib -f net10.0
|
|
```
|
|
|
|
### 2. Add Package References
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.1" />
|
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<ProjectReference Include="path/to/StellaOps.Plugin/StellaOps.Plugin.csproj" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
### 3. Create Options Class
|
|
|
|
```csharp
|
|
namespace MyCompany.StellaOps.Plugin.MyPlugin;
|
|
|
|
public sealed class MyPluginOptions
|
|
{
|
|
public string? ApiEndpoint { get; set; }
|
|
public string? ApiKey { get; set; }
|
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1);
|
|
public int MaxRetries { get; set; } = 3;
|
|
}
|
|
```
|
|
|
|
### 4. Create Plugin Implementation
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Plugin;
|
|
|
|
namespace MyCompany.StellaOps.Plugin.MyPlugin;
|
|
|
|
public sealed class MyPlugin : IAvailabilityPlugin
|
|
{
|
|
public string PluginId => "mycompany.stellaops.myplugin";
|
|
public string DisplayName => "My Custom Plugin";
|
|
|
|
public bool IsAvailable(IServiceProvider services)
|
|
{
|
|
// Check if required dependencies are available
|
|
var config = services.GetService<IConfiguration>();
|
|
var apiKey = config?["MyPlugin:ApiKey"];
|
|
return !string.IsNullOrEmpty(apiKey);
|
|
}
|
|
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
var services = context.Services;
|
|
var configuration = context.Configuration;
|
|
|
|
// Bind configuration
|
|
var section = configuration.GetSection("MyPlugin");
|
|
services.Configure<MyPluginOptions>(options =>
|
|
{
|
|
section.Bind(options);
|
|
});
|
|
|
|
// Register services
|
|
services.AddSingleton<MyPluginService>();
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Add Version Attribute
|
|
|
|
```csharp
|
|
using StellaOps.Plugin.Versioning;
|
|
|
|
[assembly: StellaPluginVersion("1.0.0", MinimumHostVersion = "1.0.0")]
|
|
```
|
|
|
|
## Registration Context
|
|
|
|
### Generic Context
|
|
|
|
```csharp
|
|
public sealed class PluginRegistrationContext
|
|
{
|
|
public IServiceCollection Services { get; }
|
|
public IConfiguration Configuration { get; }
|
|
}
|
|
```
|
|
|
|
### Router Transport Context
|
|
|
|
```csharp
|
|
public sealed class RouterTransportRegistrationContext
|
|
{
|
|
public IServiceCollection Services { get; }
|
|
public IConfiguration Configuration { get; }
|
|
public RouterTransportMode Mode { get; }
|
|
public string? ConfigurationSection { get; init; }
|
|
}
|
|
|
|
[Flags]
|
|
public enum RouterTransportMode
|
|
{
|
|
None = 0,
|
|
Server = 1,
|
|
Client = 2,
|
|
Both = Server | Client
|
|
}
|
|
```
|
|
|
|
## Configuration Binding
|
|
|
|
### Basic Binding
|
|
|
|
```csharp
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
context.Services.Configure<MyOptions>(options =>
|
|
{
|
|
context.Configuration.GetSection("MyPlugin").Bind(options);
|
|
});
|
|
}
|
|
```
|
|
|
|
### With Validation
|
|
|
|
```csharp
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
context.Services.AddOptions<MyOptions>()
|
|
.Bind(context.Configuration.GetSection("MyPlugin"))
|
|
.ValidateDataAnnotations()
|
|
.ValidateOnStart();
|
|
}
|
|
```
|
|
|
|
### Options Class with Validation
|
|
|
|
```csharp
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
public sealed class MyOptions
|
|
{
|
|
[Required]
|
|
[Url]
|
|
public string? ApiEndpoint { get; set; }
|
|
|
|
[Required]
|
|
public string? ApiKey { get; set; }
|
|
|
|
[Range(1, 100)]
|
|
public int MaxRetries { get; set; } = 3;
|
|
}
|
|
```
|
|
|
|
## DI Registration Patterns
|
|
|
|
### Singleton Services
|
|
|
|
```csharp
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
context.Services.AddSingleton<IMyService, MyService>();
|
|
}
|
|
```
|
|
|
|
### Factory Registration
|
|
|
|
```csharp
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
context.Services.AddSingleton<IMyService>(sp =>
|
|
{
|
|
var options = sp.GetRequiredService<IOptions<MyOptions>>();
|
|
var logger = sp.GetRequiredService<ILogger<MyService>>();
|
|
return new MyService(options.Value, logger);
|
|
});
|
|
}
|
|
```
|
|
|
|
### Keyed Services
|
|
|
|
```csharp
|
|
public void Register(PluginRegistrationContext context)
|
|
{
|
|
context.Services.AddKeyedSingleton<IMyService>("myplugin", (sp, key) =>
|
|
{
|
|
return new MyService();
|
|
});
|
|
}
|
|
```
|
|
|
|
## Logging
|
|
|
|
### Structured Logging
|
|
|
|
```csharp
|
|
public sealed class MyPluginService
|
|
{
|
|
private readonly ILogger<MyPluginService> _logger;
|
|
|
|
public MyPluginService(ILogger<MyPluginService> logger)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task ProcessAsync(string itemId)
|
|
{
|
|
using var scope = _logger.BeginScope(
|
|
new Dictionary<string, object>
|
|
{
|
|
["PluginId"] = "myplugin",
|
|
["ItemId"] = itemId
|
|
});
|
|
|
|
_logger.LogInformation("Processing item {ItemId}", itemId);
|
|
|
|
try
|
|
{
|
|
// Process...
|
|
_logger.LogDebug("Item {ItemId} processed successfully", itemId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to process item {ItemId}", itemId);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Availability Check
|
|
|
|
```csharp
|
|
public bool IsAvailable(IServiceProvider services)
|
|
{
|
|
try
|
|
{
|
|
var config = services.GetRequiredService<IConfiguration>();
|
|
var options = config.GetSection("MyPlugin").Get<MyOptions>();
|
|
|
|
if (string.IsNullOrEmpty(options?.ApiEndpoint))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check connectivity if needed
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "Availability check failed");
|
|
return false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Graceful Degradation
|
|
|
|
```csharp
|
|
public async Task<Result> ProcessAsync(Request request)
|
|
{
|
|
try
|
|
{
|
|
return await ProcessInternalAsync(request);
|
|
}
|
|
catch (ApiException ex) when (ex.StatusCode == 429)
|
|
{
|
|
_logger.LogWarning("Rate limited, using cached data");
|
|
return await GetCachedResultAsync(request);
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
_logger.LogWarning("Request timeout, using fallback");
|
|
return Result.Fallback();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Unit Testing
|
|
|
|
```csharp
|
|
public class MyPluginTests
|
|
{
|
|
[Fact]
|
|
public void IsAvailable_WithValidConfig_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["MyPlugin:ApiEndpoint"] = "https://api.example.com",
|
|
["MyPlugin:ApiKey"] = "test-key"
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton<IConfiguration>(config);
|
|
var provider = services.BuildServiceProvider();
|
|
|
|
var plugin = new MyPlugin();
|
|
|
|
// Act
|
|
var available = plugin.IsAvailable(provider);
|
|
|
|
// Assert
|
|
Assert.True(available);
|
|
}
|
|
|
|
[Fact]
|
|
public void Register_AddsServices()
|
|
{
|
|
// Arrange
|
|
var config = new ConfigurationBuilder().Build();
|
|
var services = new ServiceCollection();
|
|
var context = new PluginRegistrationContext(services, config);
|
|
|
|
var plugin = new MyPlugin();
|
|
|
|
// Act
|
|
plugin.Register(context);
|
|
|
|
// Assert
|
|
var provider = services.BuildServiceProvider();
|
|
Assert.NotNull(provider.GetService<MyPluginService>());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Integration Testing
|
|
|
|
```csharp
|
|
public class MyPluginIntegrationTests : IClassFixture<PluginTestFixture>
|
|
{
|
|
private readonly PluginTestFixture _fixture;
|
|
|
|
public MyPluginIntegrationTests(PluginTestFixture fixture)
|
|
{
|
|
_fixture = fixture;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessAsync_WithValidInput_ReturnsResult()
|
|
{
|
|
// Arrange
|
|
var service = _fixture.GetService<IMyService>();
|
|
var request = new Request { Id = "test-001" };
|
|
|
|
// Act
|
|
var result = await service.ProcessAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.Equal("test-001", result.RequestId);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Packaging
|
|
|
|
### Project Configuration
|
|
|
|
```xml
|
|
<PropertyGroup>
|
|
<TargetFramework>net10.0</TargetFramework>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
<Nullable>enable</Nullable>
|
|
|
|
<!-- Package metadata -->
|
|
<PackageId>MyCompany.StellaOps.Plugin.MyPlugin</PackageId>
|
|
<Version>1.0.0</Version>
|
|
<Authors>My Company</Authors>
|
|
<Description>My custom StellaOps plugin</Description>
|
|
|
|
<!-- Plugin output -->
|
|
<IsPackable>true</IsPackable>
|
|
<IncludeBuildOutput>true</IncludeBuildOutput>
|
|
</PropertyGroup>
|
|
```
|
|
|
|
### Plugin Manifest
|
|
|
|
Create `plugin.json`:
|
|
|
|
```json
|
|
{
|
|
"schemaVersion": "2.0",
|
|
"id": "mycompany.stellaops.myplugin",
|
|
"name": "My Custom Plugin",
|
|
"version": "1.0.0",
|
|
"assembly": {
|
|
"path": "MyCompany.StellaOps.Plugin.MyPlugin.dll",
|
|
"entryType": "MyCompany.StellaOps.Plugin.MyPlugin.MyPlugin"
|
|
},
|
|
"capabilities": ["custom-feature"],
|
|
"platforms": ["linux-x64", "win-x64", "osx-arm64"],
|
|
"enabled": true,
|
|
"priority": 100
|
|
}
|
|
```
|
|
|
|
### Build and Package
|
|
|
|
```bash
|
|
# Build release
|
|
dotnet build -c Release
|
|
|
|
# Create NuGet package
|
|
dotnet pack -c Release
|
|
|
|
# Publish to plugins directory
|
|
dotnet publish -c Release -o ./plugins
|
|
```
|
|
|
|
## Signing
|
|
|
|
### Sign with Cosign
|
|
|
|
```bash
|
|
# Generate key pair
|
|
cosign generate-key-pair
|
|
|
|
# Sign plugin assembly
|
|
cosign sign-blob --key cosign.key \
|
|
plugins/MyPlugin.dll \
|
|
--output-file plugins/MyPlugin.dll.sig
|
|
```
|
|
|
|
### Verify Signature
|
|
|
|
```bash
|
|
cosign verify-blob --key cosign.pub \
|
|
--signature plugins/MyPlugin.dll.sig \
|
|
plugins/MyPlugin.dll
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Determinism
|
|
|
|
- Use stable ordering for collections
|
|
- Use UTC timestamps in ISO-8601 format
|
|
- Avoid non-deterministic random values
|
|
- Cache results consistently
|
|
|
|
### Offline Support
|
|
|
|
- Don't hardcode external URLs
|
|
- Provide fallback mechanisms
|
|
- Cache remote data when possible
|
|
- Log warnings, not errors, for optional network features
|
|
|
|
### Performance
|
|
|
|
- Use async/await properly
|
|
- Implement cancellation tokens
|
|
- Pool expensive resources
|
|
- Limit memory allocations
|
|
|
|
### Security
|
|
|
|
- Validate all inputs
|
|
- Sanitize output data
|
|
- Use secure defaults
|
|
- Never log secrets
|
|
|
|
## Examples
|
|
|
|
See the [plugin templates](./plugin-templates/README.md) for complete working examples.
|
|
|
|
## See Also
|
|
|
|
- [Plugin Overview](../plugins/README.md)
|
|
- [Plugin Architecture](../plugins/ARCHITECTURE.md)
|
|
- [Plugin Configuration](../plugins/CONFIGURATION.md)
|
|
- [Router Transport Development](../router/transports/development.md)
|