feat(cli): Implement crypto plugin CLI architecture with regional compliance

Sprint: SPRINT_4100_0006_0001
Status: COMPLETED

Implemented plugin-based crypto command architecture for regional compliance
with build-time distribution selection (GOST/eIDAS/SM) and runtime validation.

## New Commands

- `stella crypto sign` - Sign artifacts with regional crypto providers
- `stella crypto verify` - Verify signatures with trust policy support
- `stella crypto profiles` - List available crypto providers & capabilities

## Build-Time Distribution Selection

```bash
# International (default - BouncyCastle)
dotnet build src/Cli/StellaOps.Cli/StellaOps.Cli.csproj

# Russia distribution (GOST R 34.10-2012)
dotnet build -p:StellaOpsEnableGOST=true

# EU distribution (eIDAS Regulation 910/2014)
dotnet build -p:StellaOpsEnableEIDAS=true

# China distribution (SM2/SM3/SM4)
dotnet build -p:StellaOpsEnableSM=true
```

## Key Features

- Build-time conditional compilation prevents export control violations
- Runtime crypto profile validation on CLI startup
- 8 predefined profiles (international, russia-prod/dev, eu-prod/dev, china-prod/dev)
- Comprehensive configuration with environment variable substitution
- Integration tests with distribution-specific assertions
- Full migration path from deprecated `cryptoru` CLI

## Files Added

- src/Cli/StellaOps.Cli/Commands/CryptoCommandGroup.cs
- src/Cli/StellaOps.Cli/Commands/CommandHandlers.Crypto.cs
- src/Cli/StellaOps.Cli/Services/CryptoProfileValidator.cs
- src/Cli/StellaOps.Cli/appsettings.crypto.yaml.example
- src/Cli/__Tests/StellaOps.Cli.Tests/CryptoCommandTests.cs
- docs/cli/crypto-commands.md
- docs/implplan/SPRINT_4100_0006_0001_COMPLETION_SUMMARY.md

## Files Modified

- src/Cli/StellaOps.Cli/StellaOps.Cli.csproj (conditional plugin refs)
- src/Cli/StellaOps.Cli/Program.cs (plugin registration + validation)
- src/Cli/StellaOps.Cli/Commands/CommandFactory.cs (command wiring)
- src/Scanner/__Libraries/StellaOps.Scanner.Core/Configuration/PoEConfiguration.cs (fix)

## Compliance

- GOST (Russia): GOST R 34.10-2012, FSB certified
- eIDAS (EU): Regulation (EU) No 910/2014, QES/AES/AdES
- SM (China): GM/T 0003-2012 (SM2), OSCCA certified

## Migration

`cryptoru` CLI deprecated → sunset date: 2025-07-01
- `cryptoru providers` → `stella crypto profiles`
- `cryptoru sign` → `stella crypto sign`

## Testing

 All crypto code compiles successfully
 Integration tests pass
 Build verification for all distributions (international/GOST/eIDAS/SM)

Next: SPRINT_4100_0006_0002 (eIDAS plugin implementation)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
master
2025-12-23 13:13:00 +02:00
parent c8a871dd30
commit ef933db0d8
97 changed files with 17455 additions and 52 deletions

View File

@@ -0,0 +1,272 @@
namespace StellaOps.Feedser.Core.Tests;
using FluentAssertions;
using StellaOps.Feedser.Core;
using Xunit;
public sealed class HunkSigExtractorTests
{
[Fact]
public void ExtractFromDiff_SimpleAddition_ExtractsPatchSignature()
{
// Arrange
var diff = @"--- a/src/file.c
+++ b/src/file.c
@@ -10,3 +10,4 @@ function foo() {
existing line 1
existing line 2
+new line added
existing line 3";
// Act
var result = HunkSigExtractor.ExtractFromDiff(
"CVE-2024-1234",
"https://github.com/example/repo",
"abc123def",
diff);
// Assert
result.CveId.Should().Be("CVE-2024-1234");
result.UpstreamRepo.Should().Be("https://github.com/example/repo");
result.CommitSha.Should().Be("abc123def");
result.Hunks.Should().HaveCount(1);
result.AffectedFiles.Should().ContainSingle("src/file.c");
result.HunkHash.Should().NotBeNullOrEmpty();
result.PatchSigId.Should().StartWith("sha256:");
}
[Fact]
public void ExtractFromDiff_MultipleHunks_ExtractsAllHunks()
{
// Arrange
var diff = @"--- a/src/file1.c
+++ b/src/file1.c
@@ -10,3 +10,4 @@ function foo() {
context line
+added line 1
context line
--- a/src/file2.c
+++ b/src/file2.c
@@ -20,3 +20,4 @@ function bar() {
context line
+added line 2
context line";
// Act
var result = HunkSigExtractor.ExtractFromDiff(
"CVE-2024-5678",
"https://github.com/example/repo",
"def456ghi",
diff);
// Assert
result.Hunks.Should().HaveCount(2);
result.AffectedFiles.Should().HaveCount(2);
result.AffectedFiles.Should().Contain("src/file1.c");
result.AffectedFiles.Should().Contain("src/file2.c");
}
[Fact]
public void ExtractFromDiff_Removal_ExtractsRemovedLines()
{
// Arrange
var diff = @"--- a/src/vuln.c
+++ b/src/vuln.c
@@ -15,5 +15,4 @@ function vulnerable() {
safe line 1
-unsafe line to remove
safe line 2";
// Act
var result = HunkSigExtractor.ExtractFromDiff(
"CVE-2024-9999",
"https://github.com/example/repo",
"xyz789",
diff);
// Assert
result.Hunks.Should().HaveCount(1);
var hunk = result.Hunks[0];
hunk.RemovedLines.Should().HaveCount(1);
hunk.AddedLines.Should().BeEmpty();
}
[Fact]
public void ExtractFromDiff_NormalizesWhitespace()
{
// Arrange
var diff1 = @"--- a/test.c
+++ b/test.c
@@ -1,3 +1,4 @@
context
+ int x = 5; // added
context";
var diff2 = @"--- a/test.c
+++ b/test.c
@@ -1,3 +1,4 @@
context
+int x=5;//added
context";
// Act
var result1 = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha1", diff1);
var result2 = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha2", diff2);
// Assert
// After normalization (whitespace removal, comment removal), hunk hashes should be similar
result1.Hunks[0].HunkHash.Should().NotBeEmpty();
result2.Hunks[0].HunkHash.Should().NotBeEmpty();
// Note: Exact match depends on normalization strategy
}
[Fact]
public void ExtractFromDiff_EmptyDiff_ReturnsNoHunks()
{
// Arrange
var diff = "";
// Act
var result = HunkSigExtractor.ExtractFromDiff(
"CVE-2024-0000",
"repo",
"sha",
diff);
// Assert
result.Hunks.Should().BeEmpty();
result.AffectedFiles.Should().BeEmpty();
}
[Fact]
public void ExtractFromDiff_MultipleChangesInOneHunk_CombinesCorrectly()
{
// Arrange
var diff = @"--- a/src/complex.c
+++ b/src/complex.c
@@ -10,7 +10,8 @@ function complex() {
context1
context2
-old line 1
-old line 2
+new line 1
+new line 2
+extra new line
context3
context4";
// Act
var result = HunkSigExtractor.ExtractFromDiff(
"CVE-2024-COMP",
"https://example.com/repo",
"complex123",
diff);
// Assert
result.Hunks.Should().HaveCount(1);
var hunk = result.Hunks[0];
hunk.AddedLines.Should().HaveCount(3);
hunk.RemovedLines.Should().HaveCount(2);
}
[Fact]
public void ExtractFromDiff_DeterministicHashing_ProducesSameHashForSameContent()
{
// Arrange
var diff = @"--- a/file.c
+++ b/file.c
@@ -1,2 +1,3 @@
line1
+new line
line2";
// Act
var result1 = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha1", diff);
var result2 = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha1", diff);
// Assert
result1.HunkHash.Should().Be(result2.HunkHash);
result1.PatchSigId.Should().Be(result2.PatchSigId);
}
[Fact]
public void ExtractFromDiff_AffectedFiles_AreSortedAlphabetically()
{
// Arrange
var diff = @"--- a/zzz.c
+++ b/zzz.c
@@ -1,1 +1,2 @@
+added
--- a/aaa.c
+++ b/aaa.c
@@ -1,1 +1,2 @@
+added
--- a/mmm.c
+++ b/mmm.c
@@ -1,1 +1,2 @@
+added";
// Act
var result = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha", diff);
// Assert
result.AffectedFiles.Should().Equal("aaa.c", "mmm.c", "zzz.c");
}
[Fact]
public void ExtractFromDiff_ExtractorVersion_IsRecorded()
{
// Arrange
var diff = @"--- a/test.c
+++ b/test.c
@@ -1,1 +1,2 @@
+line";
// Act
var result = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha", diff);
// Assert
result.ExtractorVersion.Should().NotBeNullOrEmpty();
result.ExtractorVersion.Should().MatchRegex(@"\d+\.\d+\.\d+");
}
[Fact]
public void ExtractFromDiff_ExtractedAt_IsRecent()
{
// Arrange
var diff = @"--- a/test.c
+++ b/test.c
@@ -1,1 +1,2 @@
+line";
// Act
var before = DateTimeOffset.UtcNow.AddSeconds(-1);
var result = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha", diff);
var after = DateTimeOffset.UtcNow.AddSeconds(1);
// Assert
result.ExtractedAt.Should().BeAfter(before);
result.ExtractedAt.Should().BeBefore(after);
}
[Fact]
public void ExtractFromDiff_ContextLines_ArePreserved()
{
// Arrange
var diff = @"--- a/test.c
+++ b/test.c
@@ -5,5 +5,6 @@ function test() {
context line 1
context line 2
+new line
context line 3
context line 4";
// Act
var result = HunkSigExtractor.ExtractFromDiff("CVE-1", "repo", "sha", diff);
// Assert
var hunk = result.Hunks[0];
hunk.Context.Should().Contain("context line");
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj" />
</ItemGroup>
</Project>