docs consolidation, big sln build fixes, new advisories and sprints/tasks

This commit is contained in:
master
2026-01-05 18:37:04 +02:00
parent d0a7b88398
commit d7bdca6d97
175 changed files with 10322 additions and 307 deletions

View File

@@ -4,12 +4,6 @@ using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Bun;
using StellaOps.Scanner.Analyzers.Lang.Go;
using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node;
using StellaOps.Scanner.Analyzers.Lang.DotNet;
using StellaOps.Scanner.Analyzers.Lang.Python;
namespace StellaOps.Bench.ScannerAnalyzers.Scenarios;
@@ -126,13 +120,10 @@ internal sealed class LanguageAnalyzerScenarioRunner : IScenarioRunner
var id = analyzerId.Trim().ToLowerInvariant();
return id switch
{
"bun" => static () => new BunLanguageAnalyzer(),
"java" => static () => new JavaLanguageAnalyzer(),
"go" => static () => new GoLanguageAnalyzer(),
"node" => static () => new NodeLanguageAnalyzer(),
"dotnet" => static () => new DotNetLanguageAnalyzer(),
"python" => static () => new PythonLanguageAnalyzer(),
_ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'."),
// Note: Language-specific analyzers (Bun, Java, Go, Node, DotNet, Python) are loaded via plugin system.
// Benchmarks should use plugin-loaded analyzers instead of hardcoded references.
// See LanguageAnalyzerPluginCatalog for dynamic analyzer loading.
_ => throw new InvalidOperationException($"Unsupported analyzer '{analyzerId}'. Language analyzers must be loaded via plugin system."),
};
}
}

View File

@@ -8,6 +8,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="StellaOps.Bench.ScannerAnalyzers.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang/StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="../../../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Lang.Node/StellaOps.Scanner.Analyzers.Lang.Node.csproj" />

View File

@@ -69,9 +69,19 @@ internal static partial class CommandHandlers
validator,
builderLogger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<BundleBuilder>.Instance);
// Enumerate rule files from source directory
var ruleFiles = Directory.EnumerateFiles(sources, "*.json", SearchOption.AllDirectories)
.ToList();
if (ruleFiles.Count == 0)
{
AnsiConsole.MarkupLine($"[red]Error: No rule files (*.json) found in {Markup.Escape(sources)}[/]");
return 1;
}
var buildOptions = new BundleBuildOptions
{
SourceDirectory = sources,
RuleFiles = ruleFiles,
OutputDirectory = output,
BundleId = id,
Version = bundleVersion,

View File

@@ -71,3 +71,6 @@ builder.Services.AddScoped<INotifyChannelDispatcher, WebhookChannelDispatcher>()
builder.Services.AddHostedService<DeliveryDispatchWorker>();
await builder.Build().RunAsync().ConfigureAwait(false);
// Explicit internal Program class to avoid conflicts with other projects that reference this assembly
internal sealed partial class Program { }

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
@@ -7,6 +7,10 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cronos" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Models/StellaOps.Notify.Models.csproj" />
<ProjectReference Include="../../../Notify/__Libraries/StellaOps.Notify.Persistence/StellaOps.Notify.Persistence.csproj" />

View File

@@ -20,7 +20,7 @@ public sealed class BudgetEnforcementIntegrationTests
public BudgetEnforcementIntegrationTests()
{
_ledger = new BudgetLedger(_store, NullLogger<BudgetLedger>.Instance);
_ledger = new BudgetLedger(_store, logger: NullLogger<BudgetLedger>.Instance);
}
#region Window Management Tests

View File

@@ -7,7 +7,7 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism.Abstractions;
using StellaOps.Determinism;
using StellaOps.Scanner.Storage.Models;
using StellaOps.Scanner.Storage.Repositories;

View File

@@ -26,14 +26,14 @@ internal sealed class SecretsAnalyzerStageExecutor : IScanStageExecutor
"scanner.rootfs",
};
private readonly ISecretsAnalyzer _secretsAnalyzer;
private readonly SecretsAnalyzer _secretsAnalyzer;
private readonly ScannerWorkerMetrics _metrics;
private readonly TimeProvider _timeProvider;
private readonly IOptions<ScannerWorkerOptions> _options;
private readonly ILogger<SecretsAnalyzerStageExecutor> _logger;
public SecretsAnalyzerStageExecutor(
ISecretsAnalyzer secretsAnalyzer,
SecretsAnalyzer secretsAnalyzer,
ScannerWorkerMetrics metrics,
TimeProvider timeProvider,
IOptions<ScannerWorkerOptions> options,

View File

@@ -42,6 +42,11 @@ public sealed class SecretsAnalyzer : ILanguageAnalyzer
/// </summary>
public SecretRuleset? Ruleset => _ruleset;
/// <summary>
/// Gets the ruleset version string for tracking and reporting.
/// </summary>
public string RulesetVersion => _ruleset?.Version ?? "unknown";
/// <summary>
/// Sets the ruleset to use for detection.
/// Called by SecretsAnalyzerHost after loading the bundle.
@@ -51,6 +56,58 @@ public sealed class SecretsAnalyzer : ILanguageAnalyzer
_ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
}
/// <summary>
/// Analyzes raw file content for secrets. Adapter for Worker stage executor.
/// </summary>
public async ValueTask<List<SecretFinding>> AnalyzeAsync(
byte[] content,
string relativePath,
CancellationToken ct)
{
if (!IsEnabled || content is null || content.Length == 0)
{
return new List<SecretFinding>();
}
var findings = new List<SecretFinding>();
foreach (var rule in _ruleset!.GetRulesForFile(relativePath))
{
ct.ThrowIfCancellationRequested();
var matches = await _detector.DetectAsync(content, relativePath, rule, ct);
foreach (var match in matches)
{
var confidence = MapScoreToConfidence(match.ConfidenceScore);
if (confidence < _options.Value.MinConfidence)
{
continue;
}
var maskedSecret = _masker.Mask(match.Secret);
var finding = new SecretFinding
{
RuleId = rule.Id,
RuleName = rule.Name,
Severity = rule.Severity,
Confidence = confidence,
FilePath = relativePath,
LineNumber = match.LineNumber,
ColumnStart = match.ColumnStart,
ColumnEnd = match.ColumnEnd,
MatchedText = maskedSecret,
Category = rule.Category,
DetectedAtUtc = _timeProvider.GetUtcNow()
};
findings.Add(finding);
}
}
return findings;
}
public async ValueTask AnalyzeAsync(
LanguageAnalyzerContext context,
LanguageComponentWriter writer,

View File

@@ -169,7 +169,8 @@ public sealed class TriageQueryPerformanceTests : IAsyncLifetime
CveId = i % 3 == 0 ? $"CVE-2021-{23337 + i}" : null,
RuleId = i % 3 != 0 ? $"RULE-{i:D4}" : null,
FirstSeenAt = DateTimeOffset.UtcNow.AddDays(-i),
LastSeenAt = DateTimeOffset.UtcNow.AddHours(-i)
LastSeenAt = DateTimeOffset.UtcNow.AddHours(-i),
UpdatedAt = DateTimeOffset.UtcNow.AddHours(-i)
};
findings.Add(finding);
}

View File

@@ -60,6 +60,7 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
// Arrange
await Context.Database.EnsureCreatedAsync();
var now = DateTimeOffset.UtcNow;
var finding = new TriageFinding
{
Id = Guid.NewGuid(),
@@ -67,8 +68,9 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
AssetLabel = "prod/api-gateway:1.2.3",
Purl = "pkg:npm/lodash@4.17.20",
CveId = "CVE-2021-23337",
FirstSeenAt = DateTimeOffset.UtcNow,
LastSeenAt = DateTimeOffset.UtcNow
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
// Act
@@ -90,13 +92,17 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
// Arrange
await Context.Database.EnsureCreatedAsync();
var now = DateTimeOffset.UtcNow;
var finding = new TriageFinding
{
Id = Guid.NewGuid(),
AssetId = Guid.NewGuid(),
AssetLabel = "prod/api-gateway:1.2.3",
Purl = "pkg:npm/lodash@4.17.20",
CveId = "CVE-2021-23337"
CveId = "CVE-2021-23337",
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
Context.Findings.Add(finding);
@@ -111,7 +117,7 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
Note = "Code path is not reachable per RichGraph analysis",
ActorSubject = "user:test@example.com",
ActorDisplay = "Test User",
CreatedAt = DateTimeOffset.UtcNow
CreatedAt = now
};
// Act
@@ -137,13 +143,17 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
// Arrange
await Context.Database.EnsureCreatedAsync();
var now = DateTimeOffset.UtcNow;
var finding = new TriageFinding
{
Id = Guid.NewGuid(),
AssetId = Guid.NewGuid(),
AssetLabel = "prod/api-gateway:1.2.3",
Purl = "pkg:npm/lodash@4.17.20",
CveId = "CVE-2021-23337"
CveId = "CVE-2021-23337",
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
Context.Findings.Add(finding);
@@ -160,7 +170,7 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
Verdict = TriageVerdict.Block,
Lane = TriageLane.Blocked,
Why = "High-severity CVE with network exposure",
ComputedAt = DateTimeOffset.UtcNow
ComputedAt = now
};
// Act
@@ -186,13 +196,17 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
// Arrange
await Context.Database.EnsureCreatedAsync();
var now = DateTimeOffset.UtcNow;
var finding = new TriageFinding
{
Id = Guid.NewGuid(),
AssetId = Guid.NewGuid(),
AssetLabel = "prod/api:1.0",
Purl = "pkg:npm/test@1.0.0",
CveId = "CVE-2024-0001"
CveId = "CVE-2024-0001",
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
Context.Findings.Add(finding);
@@ -200,20 +214,24 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
var decision = new TriageDecision
{
Id = Guid.NewGuid(),
FindingId = finding.Id,
Kind = TriageDecisionKind.Ack,
ReasonCode = "ACKNOWLEDGED",
ActorSubject = "user:admin"
ActorSubject = "user:admin",
CreatedAt = now
};
var riskResult = new TriageRiskResult
{
Id = Guid.NewGuid(),
FindingId = finding.Id,
PolicyId = "policy-v1",
PolicyVersion = "1.0",
InputsHash = "hash123",
Score = 50,
Why = "Medium risk"
Why = "Medium risk",
ComputedAt = now
};
Context.Decisions.Add(decision);
@@ -245,13 +263,18 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
const string purl = "pkg:npm/lodash@4.17.20";
const string cveId = "CVE-2021-23337";
var now = DateTimeOffset.UtcNow;
var finding1 = new TriageFinding
{
Id = Guid.NewGuid(),
AssetId = assetId,
EnvironmentId = envId,
AssetLabel = "prod/api:1.0",
Purl = purl,
CveId = cveId
CveId = cveId,
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
Context.Findings.Add(finding1);
@@ -259,11 +282,15 @@ public sealed class TriageSchemaIntegrationTests : IAsyncLifetime
var finding2 = new TriageFinding
{
Id = Guid.NewGuid(),
AssetId = assetId,
EnvironmentId = envId,
AssetLabel = "prod/api:1.0",
Purl = purl,
CveId = cveId
CveId = cveId,
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
Context.Findings.Add(finding2);

View File

@@ -132,6 +132,7 @@ public sealed class FindingsEvidenceControllerTests
await db.Database.EnsureCreatedAsync();
var now = DateTimeOffset.UtcNow;
var findingId = Guid.NewGuid();
var finding = new TriageFinding
{
@@ -140,12 +141,15 @@ public sealed class FindingsEvidenceControllerTests
AssetLabel = "prod/api-gateway:1.2.3",
Purl = "pkg:npm/lodash@4.17.20",
CveId = "CVE-2024-12345",
LastSeenAt = DateTimeOffset.UtcNow
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
db.Findings.Add(finding);
db.RiskResults.Add(new TriageRiskResult
{
Id = Guid.NewGuid(),
FindingId = findingId,
PolicyId = "policy-1",
PolicyVersion = "1.0.0",
@@ -154,15 +158,17 @@ public sealed class FindingsEvidenceControllerTests
Verdict = TriageVerdict.Block,
Lane = TriageLane.Blocked,
Why = "High risk score",
ComputedAt = DateTimeOffset.UtcNow
ComputedAt = now
});
db.EvidenceArtifacts.Add(new TriageEvidenceArtifact
{
Id = Guid.NewGuid(),
FindingId = findingId,
Type = TriageEvidenceType.Provenance,
Title = "SBOM attestation",
ContentHash = "sha256:attestation",
Uri = "s3://evidence/attestation.json"
Uri = "s3://evidence/attestation.json",
CreatedAt = now
});
await db.SaveChangesAsync();

View File

@@ -448,6 +448,7 @@ public sealed class GatingReasonServiceTests
public void VexEvidenceTrust_SignedWithLedger_HasHighTrust()
{
// Arrange - DSSE envelope + signature ref + source ref
var now = DateTimeOffset.UtcNow;
var vex = new TriageEffectiveVex
{
Id = Guid.NewGuid(),
@@ -455,7 +456,9 @@ public sealed class GatingReasonServiceTests
DsseEnvelopeHash = "sha256:signed",
SignatureRef = "ledger-entry",
SourceDomain = "nvd",
SourceRef = "NVD-CVE-2024-1234"
SourceRef = "NVD-CVE-2024-1234",
ValidFrom = now,
CollectedAt = now
};
// Assert - all evidence factors present
@@ -469,6 +472,7 @@ public sealed class GatingReasonServiceTests
public void VexEvidenceTrust_NoEvidence_HasBaseTrust()
{
// Arrange - no signature, no ledger, no source
var now = DateTimeOffset.UtcNow;
var vex = new TriageEffectiveVex
{
Id = Guid.NewGuid(),
@@ -476,7 +480,9 @@ public sealed class GatingReasonServiceTests
DsseEnvelopeHash = null,
SignatureRef = null,
SourceDomain = "unknown",
SourceRef = "unknown"
SourceRef = "unknown",
ValidFrom = now,
CollectedAt = now
};
// Assert - base trust only
@@ -493,12 +499,16 @@ public sealed class GatingReasonServiceTests
public void TriageFinding_RequiredFields_AreSet()
{
// Arrange
var now = DateTimeOffset.UtcNow;
var finding = new TriageFinding
{
Id = Guid.NewGuid(),
AssetLabel = "test-asset",
Purl = "pkg:npm/test@1.0.0",
CveId = "CVE-2024-1234"
CveId = "CVE-2024-1234",
FirstSeenAt = now,
LastSeenAt = now,
UpdatedAt = now
};
// Assert
@@ -519,7 +529,8 @@ public sealed class GatingReasonServiceTests
{
Id = Guid.NewGuid(),
PolicyId = "test-policy",
Action = action
Action = action,
AppliedAt = DateTimeOffset.UtcNow
};
decision.Action.Should().Be(action);
@@ -562,7 +573,8 @@ public sealed class GatingReasonServiceTests
Id = Guid.NewGuid(),
Reachable = TriageReachability.No,
InputsHash = "sha256:inputs-hash",
SubgraphId = "sha256:subgraph"
SubgraphId = "sha256:subgraph",
ComputedAt = DateTimeOffset.UtcNow
};
// Assert

View File

@@ -113,7 +113,10 @@ public sealed class LinksetResolverTests
FeatureFlags: Array.Empty<string>(),
Secrets: new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
Tenant: "tenant-a",
Tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
Tls: new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{
CreatedAtUtc = DateTimeOffset.UtcNow
};
public IReadOnlyDictionary<string, string> RawVariables { get; } = new Dictionary<string, string>(StringComparer.Ordinal)
{

View File

@@ -202,7 +202,10 @@ public sealed class ScannerSurfaceSecretConfiguratorTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("inline", "tenant", null, null, null, true),
"tenant",
new SurfaceTlsConfiguration(null, null, null));
new SurfaceTlsConfiguration(null, null, null))
{
CreatedAtUtc = DateTimeOffset.UtcNow
};
RawVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}

View File

@@ -26,7 +26,10 @@ public sealed class SurfaceCacheOptionsConfiguratorTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-b", "/etc/secrets", null, null, false),
"tenant-b",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{
CreatedAtUtc = DateTimeOffset.UtcNow
};
var environment = new StubSurfaceEnvironment(settings);
var configurator = new SurfaceCacheOptionsConfigurator(environment);

View File

@@ -28,7 +28,10 @@ public sealed class SurfaceManifestStoreOptionsConfiguratorTests
Array.Empty<string>(),
new SurfaceSecretsConfiguration("file", "tenant-a", "/etc/secrets", null, null, false),
"tenant-a",
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()));
new SurfaceTlsConfiguration(null, null, new X509Certificate2Collection()))
{
CreatedAtUtc = DateTimeOffset.UtcNow
};
var environment = new StubSurfaceEnvironment(settings);
var cacheOptions = Microsoft.Extensions.Options.Options.Create(new SurfaceCacheOptions { RootDirectory = cacheRoot.FullName });