finish secrets finding work and audit remarks work save

This commit is contained in:
StellaOps Bot
2026-01-04 21:48:13 +02:00
parent 75611a505f
commit 8862e112c4
157 changed files with 11702 additions and 416 deletions

View File

@@ -0,0 +1,542 @@
# Sprint 20260104_002_SCANNER - Secret Leak Detection Core Analyzer
## Topic & Scope
Implement the core `StellaOps.Scanner.Analyzers.Secrets` plugin that detects accidentally committed secrets in container layers during scans. This is the foundational sprint for secret leak detection capability.
**Key deliverables:**
1. **Secrets Analyzer Plugin**: Core analyzer that executes regex/entropy-based detection rules
2. **Rule Engine**: Rule definition models, matching logic, and deterministic execution
3. **Masking Engine**: Payload masking to ensure secrets never leak in outputs
4. **Evidence Emission**: `secret.leak` evidence type integration with ScanAnalysisStore
5. **Feature Flag**: Experimental toggle for gradual rollout
**Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/`
## Dependencies & Concurrency
- **Depends on**: Surface.Secrets, Surface.Validation, Surface.Env (already implemented)
- **Required by**: Sprint 20260104_003 (Rule Bundle Infrastructure), Sprint 20260104_004 (Policy DSL)
- **Parallel work**: Tasks SLD-001 through SLD-008 can be developed concurrently
- **Integration tasks** (SLD-009+) require prior tasks complete
## Documentation Prerequisites
- docs/README.md
- docs/07_HIGH_LEVEL_ARCHITECTURE.md
- docs/modules/scanner/architecture.md
- docs/modules/scanner/design/surface-secrets.md
- docs/modules/scanner/operations/secret-leak-detection.md (target spec)
- CLAUDE.md (especially Section 8: Code Quality & Determinism Rules)
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | SLD-001 | DONE | None | Scanner Guild | Create project structure and csproj |
| 2 | SLD-002 | DONE | None | Scanner Guild | Define SecretRule and SecretRuleset models |
| 3 | SLD-003 | DONE | None | Scanner Guild | Implement ISecretDetector interface and RegexDetector |
| 4 | SLD-004 | DONE | None | Scanner Guild | Implement EntropyDetector for high-entropy string detection |
| 5 | SLD-005 | DONE | None | Scanner Guild | Implement PayloadMasker with configurable masking strategies |
| 6 | SLD-006 | DONE | None | Scanner Guild | Define SecretLeakEvidence record and finding model |
| 7 | SLD-007 | DONE | SLD-002 | Scanner Guild | Implement RulesetLoader with JSON parsing |
| 8 | SLD-008 | DONE | None | Scanner Guild | Add SecretsAnalyzerOptions with feature flag support |
| 9 | SLD-009 | DONE | SLD-003,SLD-004 | Scanner Guild | Implement CompositeSecretDetector combining regex and entropy |
| 10 | SLD-010 | DONE | SLD-006,SLD-009 | Scanner Guild | Implement SecretsAnalyzer (ILanguageAnalyzer) |
| 11 | SLD-011 | DONE | SLD-010 | Scanner Guild | Add SecretsAnalyzerHost for plugin lifecycle |
| 12 | SLD-012 | DONE | SLD-011 | Scanner Guild | Integrate with Scanner Worker pipeline |
| 13 | SLD-013 | DONE | SLD-010 | Scanner Guild | Add DI registration in ServiceCollectionExtensions |
| 14 | SLD-014 | DONE | All | Scanner Guild | Add comprehensive unit tests |
| 15 | SLD-015 | DONE | SLD-014 | Scanner Guild | Add integration tests with test fixtures |
| 16 | SLD-016 | DONE | All | Scanner Guild | Create AGENTS.md for module |
## Task Details
### SLD-001: Project Structure
Create the project skeleton following Scanner conventions:
```
src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/
├── StellaOps.Scanner.Analyzers.Secrets.csproj
├── AGENTS.md
├── AssemblyInfo.cs
├── Detectors/
│ ├── ISecretDetector.cs
│ ├── RegexDetector.cs
│ ├── EntropyDetector.cs
│ └── CompositeSecretDetector.cs
├── Rules/
│ ├── SecretRule.cs
│ ├── SecretRuleset.cs
│ └── RulesetLoader.cs
├── Masking/
│ ├── IPayloadMasker.cs
│ └── PayloadMasker.cs
├── Evidence/
│ ├── SecretLeakEvidence.cs
│ └── SecretFinding.cs
├── SecretsAnalyzer.cs
├── SecretsAnalyzerHost.cs
├── SecretsAnalyzerOptions.cs
└── ServiceCollectionExtensions.cs
```
csproj should reference:
- StellaOps.Scanner.Core
- StellaOps.Scanner.Surface
- StellaOps.Evidence.Core
### SLD-002: Rule Models
Define the rule structure for secret detection:
```csharp
/// <summary>
/// A single secret detection rule.
/// </summary>
public sealed record SecretRule
{
public required string Id { get; init; } // e.g., "stellaops.secrets.aws-access-key"
public required string Version { get; init; } // e.g., "2025.11.0"
public required string Name { get; init; } // Human-readable name
public required string Description { get; init; }
public required SecretRuleType Type { get; init; } // Regex, Entropy, Composite
public required string Pattern { get; init; } // Regex pattern or entropy config
public required SecretSeverity Severity { get; init; }
public required SecretConfidence Confidence { get; init; }
public string? MaskingHint { get; init; } // e.g., "prefix:4,suffix:2"
public ImmutableArray<string> Keywords { get; init; } // Pre-filter keywords
public ImmutableArray<string> FilePatterns { get; init; } // Glob patterns for file filtering
public bool Enabled { get; init; } = true;
}
public enum SecretRuleType { Regex, Entropy, Composite }
public enum SecretSeverity { Low, Medium, High, Critical }
public enum SecretConfidence { Low, Medium, High }
/// <summary>
/// A versioned collection of secret detection rules.
/// </summary>
public sealed record SecretRuleset
{
public required string Id { get; init; } // e.g., "secrets.ruleset"
public required string Version { get; init; } // e.g., "2025.11"
public required DateTimeOffset CreatedAt { get; init; }
public required ImmutableArray<SecretRule> Rules { get; init; }
public string? Sha256Digest { get; init; } // Integrity hash
}
```
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Rules/`
### SLD-003: Regex Detector
Implement regex-based secret detection:
```csharp
public interface ISecretDetector
{
string DetectorId { get; }
ValueTask<IReadOnlyList<SecretMatch>> DetectAsync(
ReadOnlyMemory<byte> content,
string filePath,
SecretRule rule,
CancellationToken ct = default);
}
public sealed record SecretMatch(
SecretRule Rule,
string FilePath,
int LineNumber,
int ColumnStart,
int ColumnEnd,
ReadOnlyMemory<byte> RawMatch, // For masking
double ConfidenceScore);
public sealed class RegexDetector : ISecretDetector
{
public string DetectorId => "regex";
// Implementation notes:
// - Use compiled regex for performance
// - Apply keyword pre-filter before regex matching
// - Respect file pattern filters
// - Track line/column for precise location
// - Never log raw match content
}
```
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Detectors/`
### SLD-004: Entropy Detector
Implement Shannon entropy-based detection for high-entropy strings:
```csharp
public sealed class EntropyDetector : ISecretDetector
{
public string DetectorId => "entropy";
// Implementation notes:
// - Calculate Shannon entropy for candidate strings
// - Default threshold: 4.5 bits per character
// - Minimum length: 16 characters
// - Skip common high-entropy non-secrets (UUIDs, hashes in comments)
// - Apply charset detection (base64, hex, alphanumeric)
}
public static class EntropyCalculator
{
/// <summary>
/// Calculates Shannon entropy in bits per character.
/// </summary>
public static double Calculate(ReadOnlySpan<byte> data)
{
// Use CultureInfo.InvariantCulture for all formatting
// Return 0.0 for empty input
}
}
```
### SLD-005: Payload Masker
Implement secure payload masking:
```csharp
public interface IPayloadMasker
{
/// <summary>
/// Masks a secret payload preserving prefix/suffix for identification.
/// </summary>
/// <param name="payload">The raw secret bytes</param>
/// <param name="hint">Optional masking hint from rule (e.g., "prefix:4,suffix:2")</param>
/// <returns>Masked string (e.g., "AKIA****B7")</returns>
string Mask(ReadOnlySpan<byte> payload, string? hint = null);
}
public sealed class PayloadMasker : IPayloadMasker
{
// Default: preserve first 4 and last 2 characters
// Replace middle with asterisks (max 8 asterisks)
// Minimum output length: 8 characters
// Never expose more than 6 characters total
public const int DefaultPrefixLength = 4;
public const int DefaultSuffixLength = 2;
public const int MaxMaskLength = 8;
public const char MaskChar = '*';
}
```
### SLD-006: Evidence Models
Define the evidence structure for policy integration:
```csharp
/// <summary>
/// Evidence record for a detected secret leak.
/// </summary>
public sealed record SecretLeakEvidence
{
public required string EvidenceType => "secret.leak";
public required string RuleId { get; init; }
public required string RuleVersion { get; init; }
public required SecretSeverity Severity { get; init; }
public required SecretConfidence Confidence { get; init; }
public required string FilePath { get; init; }
public required int LineNumber { get; init; }
public required string Mask { get; init; } // Masked payload
public required string BundleId { get; init; }
public required string BundleVersion { get; init; }
public required DateTimeOffset DetectedAt { get; init; }
public ImmutableDictionary<string, string>? Metadata { get; init; }
}
/// <summary>
/// Aggregated finding for a single secret match.
/// </summary>
public sealed record SecretFinding
{
public required Guid Id { get; init; }
public required SecretLeakEvidence Evidence { get; init; }
public required string ScanId { get; init; }
public required string TenantId { get; init; }
public required string ArtifactDigest { get; init; }
}
```
### SLD-007: Ruleset Loader
Implement deterministic ruleset loading:
```csharp
public interface IRulesetLoader
{
ValueTask<SecretRuleset> LoadAsync(
string rulesetPath,
CancellationToken ct = default);
ValueTask<SecretRuleset> LoadFromJsonlAsync(
Stream rulesStream,
string bundleId,
string bundleVersion,
CancellationToken ct = default);
}
public sealed class RulesetLoader : IRulesetLoader
{
// Implementation notes:
// - Parse secrets.ruleset.rules.jsonl (NDJSON format)
// - Validate rule schema on load
// - Sort rules by ID for deterministic ordering
// - Calculate and verify SHA-256 digest
// - Use CultureInfo.InvariantCulture for all parsing
// - Log bundle version on successful load
}
```
### SLD-008: Analyzer Options
Configuration options with feature flag:
```csharp
public sealed class SecretsAnalyzerOptions
{
/// <summary>
/// Enable secret leak detection (experimental feature).
/// </summary>
public bool Enabled { get; set; } = false;
/// <summary>
/// Path to the ruleset bundle directory.
/// </summary>
public string RulesetPath { get; set; } = "/opt/stellaops/plugins/scanner/analyzers/secrets";
/// <summary>
/// Minimum confidence level to report findings.
/// </summary>
public SecretConfidence MinConfidence { get; set; } = SecretConfidence.Medium;
/// <summary>
/// Maximum findings per scan (circuit breaker).
/// </summary>
public int MaxFindingsPerScan { get; set; } = 1000;
/// <summary>
/// File size limit for scanning (bytes).
/// </summary>
public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB
/// <summary>
/// Enable entropy-based detection.
/// </summary>
public bool EnableEntropyDetection { get; set; } = true;
/// <summary>
/// Entropy threshold (bits per character).
/// </summary>
public double EntropyThreshold { get; set; } = 4.5;
}
```
### SLD-009: Composite Detector
Combine multiple detection strategies:
```csharp
public sealed class CompositeSecretDetector : ISecretDetector
{
private readonly IReadOnlyList<ISecretDetector> _detectors;
private readonly ILogger<CompositeSecretDetector> _logger;
public string DetectorId => "composite";
// Implementation notes:
// - Execute detectors in parallel where possible
// - Deduplicate overlapping matches
// - Merge confidence scores for overlapping detections
// - Respect per-rule detector type preference
}
```
### SLD-010: Secrets Analyzer
Main analyzer implementation:
```csharp
public sealed class SecretsAnalyzer : ILayerAnalyzer
{
public string AnalyzerId => "secrets";
public string DisplayName => "Secret Leak Detector";
// Implementation notes:
// - Check feature flag before processing
// - Load ruleset once at startup (cached)
// - Apply file pattern filters efficiently
// - Execute detection on text files only
// - Emit SecretLeakEvidence for each finding
// - Apply masking before any output
// - Track metrics: scanner.secret.finding_total
// - Add tracing span: scanner.secrets.scan
}
```
### SLD-011: Analyzer Host
Lifecycle management for the analyzer:
```csharp
public sealed class SecretsAnalyzerHost : IHostedService
{
// Implementation notes:
// - Load and validate ruleset on startup
// - Log bundle version and rule count
// - Verify DSSE signature if available
// - Graceful shutdown with finding flush
// - Emit startup log: "SecretsAnalyzerHost: Loaded bundle {version} with {count} rules"
}
```
### SLD-012: Worker Integration
Integrate with Scanner Worker pipeline:
```csharp
// In Scanner.Worker processing pipeline:
// 1. Add SecretsAnalyzer to analyzer chain (after language analyzers)
// 2. Gate execution on feature flag
// 3. Store findings in ScanAnalysisStore
// 4. Include in scan completion event
```
### SLD-013: DI Registration
```csharp
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSecretsAnalyzer(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddOptions<SecretsAnalyzerOptions>()
.Bind(configuration.GetSection("Scanner:Analyzers:Secrets"))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IPayloadMasker, PayloadMasker>();
services.AddSingleton<IRulesetLoader, RulesetLoader>();
services.AddSingleton<ISecretDetector, RegexDetector>();
services.AddSingleton<ISecretDetector, EntropyDetector>();
services.AddSingleton<ISecretDetector, CompositeSecretDetector>();
services.AddSingleton<SecretsAnalyzer>();
services.AddHostedService<SecretsAnalyzerHost>();
return services;
}
}
```
### SLD-014: Unit Tests
Required test coverage in `src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Secrets.Tests/`:
```
├── Detectors/
│ ├── RegexDetectorTests.cs
│ ├── EntropyDetectorTests.cs
│ ├── EntropyCalculatorTests.cs
│ └── CompositeSecretDetectorTests.cs
├── Rules/
│ ├── SecretRuleTests.cs
│ └── RulesetLoaderTests.cs
├── Masking/
│ └── PayloadMaskerTests.cs
├── Evidence/
│ └── SecretLeakEvidenceTests.cs
├── SecretsAnalyzerTests.cs
└── Fixtures/
├── aws-access-key.txt
├── github-token.txt
├── private-key.pem
└── test-ruleset.jsonl
```
Test requirements:
- All tests must be deterministic
- Use `[Trait("Category", "Unit")]` for unit tests
- Test masking never exposes full secrets
- Test entropy calculation with known inputs
- Test regex patterns match expected secrets
### SLD-015: Integration Tests
Integration tests with Scanner Worker:
```
├── SecretsAnalyzerIntegrationTests.cs
│ - Test full scan with secrets embedded
│ - Verify findings in ScanAnalysisStore
│ - Verify masking in output
│ - Test feature flag disables analyzer
├── RulesetLoadingTests.cs
│ - Test loading from file system
│ - Test invalid ruleset handling
│ - Test missing bundle handling
```
### SLD-016: Module AGENTS.md
Create `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/AGENTS.md` with:
- Mission statement
- Scope definition
- Required reading list
- Working agreements
- Security considerations
## Built-in Rule Examples
Initial rules to include in default bundle:
| Rule ID | Pattern Type | Description |
|---------|--------------|-------------|
| `stellaops.secrets.aws-access-key` | Regex | AWS Access Key ID (AKIA...) |
| `stellaops.secrets.aws-secret-key` | Regex + Entropy | AWS Secret Access Key |
| `stellaops.secrets.github-pat` | Regex | GitHub Personal Access Token |
| `stellaops.secrets.github-app` | Regex | GitHub App Token (ghs_, ghp_) |
| `stellaops.secrets.gitlab-pat` | Regex | GitLab Personal Access Token |
| `stellaops.secrets.private-key-rsa` | Regex | RSA Private Key (PEM) |
| `stellaops.secrets.private-key-ec` | Regex | EC Private Key (PEM) |
| `stellaops.secrets.jwt` | Regex + Entropy | JSON Web Token |
| `stellaops.secrets.basic-auth` | Regex | Basic Auth credentials in URLs |
| `stellaops.secrets.generic-api-key` | Entropy | High-entropy API key patterns |
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Use NDJSON for rule format | Line-based parsing, easy streaming, git-friendly diffs |
| Mask before any persistence | Defense in depth - secrets never stored |
| Feature flag default off | Safe rollout, tenant opt-in required |
| Entropy threshold 4.5 bits | Balance between false positives and detection rate |
| Max 1000 findings per scan | Circuit breaker prevents DoS on noisy images |
| Text files only | Binary secret detection deferred to future sprint |
## Metrics & Observability
| Metric | Type | Labels |
|--------|------|--------|
| `scanner.secret.finding_total` | Counter | tenant, ruleId, severity, confidence |
| `scanner.secret.scan_duration_seconds` | Histogram | tenant |
| `scanner.secret.rules_loaded` | Gauge | bundleVersion |
| `scanner.secret.files_scanned` | Counter | tenant |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Based on gap analysis of secrets scanning support |
| 2026-01-04 | SLD-001 to SLD-014, SLD-016 completed | Full implementation: project structure, rule models, RegexDetector, EntropyDetector, PayloadMasker, SecretLeakEvidence, RulesetLoader, SecretsAnalyzerOptions, CompositeSecretDetector, SecretsAnalyzer, SecretsAnalyzerHost, ServiceCollectionExtensions, unit tests (EntropyCalculatorTests, PayloadMaskerTests, RegexDetectorTests, RulesetLoaderTests, SecretRuleTests, SecretRulesetTests), AGENTS.md. All builds verified. |
| 2026-01-04 | SLD-015 completed | Created integration test project with test fixtures (aws-access-key.txt, github-token.txt, private-key.pem, test-ruleset.jsonl) and SecretsAnalyzerIntegrationTests.cs covering full scan detection, feature flags, circuit breaker, masking, evidence fields, and determinism. All builds verified. **Sprint complete.** |

View File

@@ -0,0 +1,451 @@
# Sprint 20260104_003_SCANNER - Secret Detection Rule Bundle Infrastructure
## Topic & Scope
Implement the DSSE-signed rule bundle infrastructure for secret leak detection. This sprint delivers the signing, verification, and distribution pipeline for deterministic rule bundles.
**Key deliverables:**
1. **Bundle Schema**: Formal JSON schema for rule bundles
2. **Bundle Builder**: CLI tool to create and sign rule bundles
3. **DSSE Integration**: Signing and verification using existing Signer/Attestor modules
4. **Bundle Verification**: Runtime verification of bundle integrity and provenance
5. **Default Bundle**: Initial set of secret detection rules
**Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/`, `src/Cli/`, `offline/rules/secrets/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_002 (Core Analyzer), StellaOps.Attestor, StellaOps.Signer
- **Required by**: Sprint 20260104_005 (Offline Kit Integration)
- **Parallel work**: Tasks RB-001 through RB-005 can be developed concurrently
## Documentation Prerequisites
- docs/modules/scanner/operations/secret-leak-detection.md
- docs/modules/attestor/architecture.md
- docs/modules/signer/architecture.md
- docs/ci/dsse-build-flow.md
- CLAUDE.md Section 8.6 (DSSE PAE Consistency)
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | RB-001 | DONE | None | Scanner Guild | Define bundle manifest JSON schema |
| 2 | RB-002 | DONE | None | Scanner Guild | Define rules JSONL schema and validation |
| 3 | RB-003 | DONE | RB-001,RB-002 | Scanner Guild | Create BundleBuilder class for bundle creation |
| 4 | RB-004 | DONE | RB-003 | Scanner Guild | Add DSSE signing integration |
| 5 | RB-005 | DONE | RB-004 | Scanner Guild | Implement BundleVerifier with Attestor integration |
| 6 | RB-006 | DONE | RB-005 | CLI Guild | Add `stella secrets bundle create` CLI command |
| 7 | RB-007 | DONE | RB-005 | CLI Guild | Add `stella secrets bundle verify` CLI command |
| 8 | RB-008 | DONE | RB-005 | Scanner Guild | Integrate verification into SecretsAnalyzerHost |
| 9 | RB-009 | DONE | RB-002 | Scanner Guild | Create default rule definitions |
| 10 | RB-010 | DONE | RB-009 | Scanner Guild | Build and sign initial bundle (2026.01) |
| 11 | RB-011 | DONE | All | Scanner Guild | Add unit and integration tests |
| 12 | RB-012 | DONE | RB-010 | Docs Guild | Document bundle lifecycle and rotation |
## Task Details
### RB-001: Bundle Manifest Schema
Define the manifest schema (`secrets.ruleset.manifest.json`):
```json
{
"$schema": "https://stellaops.io/schemas/secrets-ruleset-manifest-v1.json",
"schemaVersion": "1.0",
"id": "secrets.ruleset",
"version": "2026.01",
"createdAt": "2026-01-04T00:00:00Z",
"description": "StellaOps Secret Detection Rules",
"rules": [
{
"id": "stellaops.secrets.aws-access-key",
"version": "1.0.0",
"severity": "high",
"enabled": true
}
],
"integrity": {
"rulesFile": "secrets.ruleset.rules.jsonl",
"rulesSha256": "abc123...",
"totalRules": 15,
"enabledRules": 15
},
"signatures": {
"dsseEnvelope": "secrets.ruleset.dsse.json"
}
}
```
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Bundles/Schemas/`
### RB-002: Rules JSONL Schema
Define the rule entry schema for NDJSON format:
```json
{
"id": "stellaops.secrets.aws-access-key",
"version": "1.0.0",
"name": "AWS Access Key ID",
"description": "Detects AWS Access Key IDs in source code and configuration files",
"type": "regex",
"pattern": "(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}",
"severity": "high",
"confidence": "high",
"keywords": ["AKIA", "ASIA", "AIDA", "aws"],
"filePatterns": ["*.yml", "*.yaml", "*.json", "*.env", "*.properties", "*.conf", "*.config"],
"maskingHint": "prefix:4,suffix:2",
"metadata": {
"category": "cloud-credentials",
"provider": "aws",
"references": ["https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"]
}
}
```
Validation rules:
- `id` must be namespaced (e.g., `stellaops.secrets.*`)
- `version` must be valid SemVer
- `pattern` must be valid regex (compile-time validation)
- `severity` must be one of: low, medium, high, critical
- `confidence` must be one of: low, medium, high
### RB-003: Bundle Builder
Implement the bundle creation logic:
```csharp
public interface IBundleBuilder
{
/// <summary>
/// Creates a bundle from individual rule files.
/// </summary>
Task<BundleArtifact> BuildAsync(
BundleBuildOptions options,
CancellationToken ct = default);
}
public sealed record BundleBuildOptions
{
public required string OutputDirectory { get; init; }
public required string BundleId { get; init; }
public required string Version { get; init; }
public required IReadOnlyList<string> RuleFiles { get; init; }
public TimeProvider? TimeProvider { get; init; }
}
public sealed record BundleArtifact
{
public required string ManifestPath { get; init; }
public required string RulesPath { get; init; }
public required string RulesSha256 { get; init; }
public required int TotalRules { get; init; }
}
public sealed class BundleBuilder : IBundleBuilder
{
// Implementation notes:
// - Validate each rule on load
// - Sort rules by ID for deterministic output
// - Compute SHA-256 of rules file
// - Generate manifest with integrity info
// - Use TimeProvider for timestamps (determinism)
}
```
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Bundles/`
### RB-004: DSSE Signing Integration
Integrate with Signer module for bundle signing:
```csharp
public interface IBundleSigner
{
/// <summary>
/// Signs a bundle artifact producing a DSSE envelope.
/// </summary>
Task<DsseEnvelope> SignAsync(
BundleArtifact artifact,
SigningOptions options,
CancellationToken ct = default);
}
public sealed record SigningOptions
{
public required string KeyId { get; init; }
public string PayloadType { get; init; } = "application/vnd.stellaops.secrets-ruleset+json";
public bool IncludeCertificateChain { get; init; } = true;
}
public sealed class BundleSigner : IBundleSigner
{
private readonly ISigner _signer;
// Implementation notes:
// - Use existing Signer infrastructure
// - Payload is the manifest JSON (not rules file)
// - Include rules file digest in signed payload
// - Support multiple signature algorithms
}
```
### RB-005: Bundle Verifier
Implement verification with Attestor integration:
```csharp
public interface IBundleVerifier
{
/// <summary>
/// Verifies a bundle's DSSE signature and integrity.
/// </summary>
Task<BundleVerificationResult> VerifyAsync(
string bundleDirectory,
VerificationOptions options,
CancellationToken ct = default);
}
public sealed record VerificationOptions
{
public string? AttestorUrl { get; init; }
public bool RequireRekorProof { get; init; } = false;
public IReadOnlyList<string>? TrustedKeyIds { get; init; }
}
public sealed record BundleVerificationResult
{
public required bool IsValid { get; init; }
public required string BundleVersion { get; init; }
public required DateTimeOffset SignedAt { get; init; }
public required string SignerKeyId { get; init; }
public string? RekorLogId { get; init; }
public IReadOnlyList<string>? ValidationErrors { get; init; }
}
public sealed class BundleVerifier : IBundleVerifier
{
private readonly IAttestorClient _attestorClient;
// Implementation notes:
// - Verify DSSE envelope signature
// - Verify rules file SHA-256 matches manifest
// - Optionally verify Rekor transparency log entry
// - Support offline verification (no network calls)
}
```
### RB-006: CLI Bundle Create Command
Add CLI command for bundle creation:
```bash
stella secrets bundle create \
--output ./bundles/2026.01 \
--bundle-id secrets.ruleset \
--version 2026.01 \
--rules ./rules/*.json \
--sign \
--key-id stellaops-secrets-signer
```
Implementation in `src/Cli/StellaOps.Cli/Commands/Secrets/`:
```csharp
[Command("secrets bundle create")]
public class BundleCreateCommand
{
[Option("--output", Required = true)]
public string OutputDirectory { get; set; }
[Option("--bundle-id", Required = true)]
public string BundleId { get; set; }
[Option("--version", Required = true)]
public string Version { get; set; }
[Option("--rules", Required = true)]
public IReadOnlyList<string> RuleFiles { get; set; }
[Option("--sign")]
public bool Sign { get; set; }
[Option("--key-id")]
public string? KeyId { get; set; }
}
```
### RB-007: CLI Bundle Verify Command
Add CLI command for bundle verification:
```bash
stella secrets bundle verify \
--bundle ./bundles/2026.01 \
--attestor-url http://attestor.local \
--require-rekor
```
```csharp
[Command("secrets bundle verify")]
public class BundleVerifyCommand
{
[Option("--bundle", Required = true)]
public string BundleDirectory { get; set; }
[Option("--attestor-url")]
public string? AttestorUrl { get; set; }
[Option("--require-rekor")]
public bool RequireRekor { get; set; }
}
```
### RB-008: Analyzer Host Integration
Update SecretsAnalyzerHost to verify bundles on startup:
```csharp
public sealed class SecretsAnalyzerHost : IHostedService
{
public async Task StartAsync(CancellationToken ct)
{
var bundlePath = _options.RulesetPath;
// Verify bundle integrity
var verification = await _verifier.VerifyAsync(bundlePath, new VerificationOptions
{
RequireRekorProof = _options.RequireSignatureVerification
}, ct);
if (!verification.IsValid)
{
_logger.LogError("Bundle verification failed: {Errors}",
string.Join(", ", verification.ValidationErrors ?? []));
if (_options.FailOnInvalidBundle)
throw new InvalidOperationException("Secret detection bundle verification failed");
return; // Analyzer disabled
}
_logger.LogInformation(
"SecretsAnalyzerHost: Loaded bundle {Version} signed by {KeyId} with {Count} rules",
verification.BundleVersion,
verification.SignerKeyId,
_ruleset.Rules.Length);
}
}
```
### RB-009: Default Rule Definitions
Create initial rule set in `offline/rules/secrets/sources/`:
| File | Rule ID | Description |
|------|---------|-------------|
| `aws-access-key.json` | `stellaops.secrets.aws-access-key` | AWS Access Key ID |
| `aws-secret-key.json` | `stellaops.secrets.aws-secret-key` | AWS Secret Access Key |
| `github-pat.json` | `stellaops.secrets.github-pat` | GitHub Personal Access Token |
| `github-app.json` | `stellaops.secrets.github-app` | GitHub App Token (ghs_, ghp_) |
| `gitlab-pat.json` | `stellaops.secrets.gitlab-pat` | GitLab Personal Access Token |
| `azure-storage.json` | `stellaops.secrets.azure-storage-key` | Azure Storage Account Key |
| `gcp-service-account.json` | `stellaops.secrets.gcp-service-account` | GCP Service Account JSON |
| `private-key-rsa.json` | `stellaops.secrets.private-key-rsa` | RSA Private Key (PEM) |
| `private-key-ec.json` | `stellaops.secrets.private-key-ec` | EC Private Key (PEM) |
| `private-key-openssh.json` | `stellaops.secrets.private-key-openssh` | OpenSSH Private Key |
| `jwt.json` | `stellaops.secrets.jwt` | JSON Web Token |
| `slack-token.json` | `stellaops.secrets.slack-token` | Slack API Token |
| `stripe-key.json` | `stellaops.secrets.stripe-key` | Stripe API Key |
| `sendgrid-key.json` | `stellaops.secrets.sendgrid-key` | SendGrid API Key |
| `generic-api-key.json` | `stellaops.secrets.generic-api-key` | Generic high-entropy API key |
### RB-010: Initial Bundle Build
Create the signed 2026.01 bundle:
```
offline/rules/secrets/2026.01/
├── secrets.ruleset.manifest.json
├── secrets.ruleset.rules.jsonl
└── secrets.ruleset.dsse.json
```
Build script in `.gitea/scripts/build/build-secrets-bundle.sh`:
```bash
#!/bin/bash
set -euo pipefail
VERSION="${1:-2026.01}"
OUTPUT_DIR="offline/rules/secrets/${VERSION}"
stella secrets bundle create \
--output "${OUTPUT_DIR}" \
--bundle-id secrets.ruleset \
--version "${VERSION}" \
--rules offline/rules/secrets/sources/*.json \
--sign \
--key-id stellaops-secrets-signer
```
### RB-011: Tests
Unit tests:
- `BundleBuilderTests.cs` - Bundle creation and validation
- `BundleVerifierTests.cs` - Signature verification
- `RuleValidatorTests.cs` - Rule schema validation
Integration tests:
- `BundleRoundtripTests.cs` - Create, sign, verify cycle
- `CliCommandTests.cs` - CLI command execution
### RB-012: Documentation
Update `docs/modules/scanner/operations/secret-leak-detection.md`:
- Add bundle creation workflow
- Document verification process
- Add troubleshooting for signature failures
Create `docs/modules/scanner/operations/secrets-bundle-rotation.md`:
- Bundle versioning strategy
- Rotation procedures
- Rollback instructions
## Bundle Directory Structure
```
offline/rules/secrets/
├── sources/ # Source rule definitions (not distributed)
│ ├── aws-access-key.json
│ ├── aws-secret-key.json
│ └── ...
├── 2026.01/ # Signed release bundle
│ ├── secrets.ruleset.manifest.json
│ ├── secrets.ruleset.rules.jsonl
│ └── secrets.ruleset.dsse.json
└── latest -> 2026.01 # Symlink to latest stable
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| NDJSON for rules | Streaming parse, git-friendly, easy validation |
| Sign manifest (not rules) | Manifest includes rules digest; smaller signature payload |
| Optional Rekor verification | Supports air-gapped deployments |
| Symlink for latest | Simple upgrade path, atomic switch |
| Source rules in repo | Version control, review process for rule changes |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Part of secret leak detection implementation |
| 2026-01-04 | RB-001 to RB-010 | Implemented bundle infrastructure, signing, verification, CLI commands |
| 2026-01-04 | RB-011 | Fixed and validated 37 unit tests for bundle system |
| 2026-01-04 | RB-012 | Updated secret-leak-detection.md, created secrets-bundle-rotation.md |
| 2026-01-04 | Sprint completed | All 12 tasks DONE |

View File

@@ -0,0 +1,549 @@
# Sprint 20260104_004_POLICY - Secret Leak Detection Policy DSL Integration
## Topic & Scope
Extend the Policy Engine and stella-dsl with `secret.*` predicates to enable policy-driven decisions on secret leak findings. This sprint delivers the policy integration layer.
**Key deliverables:**
1. **Policy Predicates**: `secret.hasFinding()`, `secret.bundle.version()`, `secret.match.count()`, `secret.mask.applied`
2. **Evidence Binding**: Connect SecretLeakEvidence to policy evaluation context
3. **Example Policies**: Sample policies for common secret blocking/warning scenarios
4. **Policy Validation**: Schema updates for secret-related predicates
**Working directory:** `src/Policy/`, `src/PolicyDsl/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_002 (Core Analyzer - SecretLeakEvidence model)
- **Parallel with**: Sprint 20260104_003 (Rule Bundles)
- **Required by**: Sprint 20260104_005 (Offline Kit Integration)
## Documentation Prerequisites
- docs/modules/policy/architecture.md
- docs/policy/dsl.md
- docs/modules/policy/secret-leak-detection-readiness.md
- CLAUDE.md Section 8 (Code Quality)
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | PSD-001 | DONE | None | Policy Guild | Define ISecretEvidenceProvider interface |
| 2 | PSD-002 | DONE | PSD-001 | Policy Guild | Implement SecretEvidenceContext binding |
| 3 | PSD-003 | DONE | None | Policy Guild | Add secret.hasFinding() predicate |
| 4 | PSD-004 | DONE | None | Policy Guild | Add secret.bundle.version() predicate |
| 5 | PSD-005 | DONE | None | Policy Guild | Add secret.match.count() predicate |
| 6 | PSD-006 | DONE | None | Policy Guild | Add secret.mask.applied predicate |
| 7 | PSD-007 | DONE | None | Policy Guild | Add secret.path.allowlist() predicate |
| 8 | PSD-008 | DONE | PSD-003-007 | Policy Guild | Register predicates in PolicyDslRegistry |
| 9 | PSD-009 | DONE | PSD-008 | Policy Guild | Update DSL schema validation |
| 10 | PSD-010 | DONE | PSD-008 | Policy Guild | Create example policy templates |
| 11 | PSD-011 | DONE | All | Policy Guild | Add unit and integration tests |
| 12 | PSD-012 | DONE | All | Docs Guild | Update policy/dsl.md documentation |
## Task Details
### PSD-001: Secret Evidence Provider Interface
Define the interface for secret evidence access:
```csharp
/// <summary>
/// Provides secret leak evidence for policy evaluation.
/// </summary>
public interface ISecretEvidenceProvider
{
/// <summary>
/// Gets all secret findings for the current evaluation context.
/// </summary>
IReadOnlyList<SecretLeakEvidence> GetFindings();
/// <summary>
/// Gets the active rule bundle metadata.
/// </summary>
SecretBundleMetadata? GetBundleMetadata();
/// <summary>
/// Checks if masking was successfully applied to all findings.
/// </summary>
bool IsMaskingApplied();
}
public sealed record SecretBundleMetadata(
string BundleId,
string Version,
DateTimeOffset SignedAt,
int RuleCount);
```
Location: `src/Policy/__Libraries/StellaOps.Policy/Secrets/`
### PSD-002: Evidence Context Binding
Bind secret evidence to the policy evaluation context:
```csharp
public sealed class SecretEvidenceContext
{
private readonly ISecretEvidenceProvider _provider;
public SecretEvidenceContext(ISecretEvidenceProvider provider)
{
_provider = provider;
}
public IReadOnlyList<SecretLeakEvidence> Findings => _provider.GetFindings();
public SecretBundleMetadata? Bundle => _provider.GetBundleMetadata();
public bool MaskingApplied => _provider.IsMaskingApplied();
}
// Integration with PolicyEvaluationContext
public sealed class PolicyEvaluationContext
{
// ... existing properties ...
public SecretEvidenceContext? Secrets { get; init; }
}
```
### PSD-003: hasFinding Predicate
Implement the `secret.hasFinding()` predicate:
```csharp
/// <summary>
/// Returns true if any secret finding matches the filter criteria.
/// </summary>
/// <example>
/// secret.hasFinding() // Any finding
/// secret.hasFinding(severity: "high") // High severity
/// secret.hasFinding(ruleId: "stellaops.secrets.aws-*") // AWS rules (glob)
/// secret.hasFinding(severity: "high", confidence: "high") // Both filters
/// </example>
[DslPredicate("secret.hasFinding")]
public sealed class SecretHasFindingPredicate : IPolicyPredicate
{
public bool Evaluate(
PolicyEvaluationContext context,
IReadOnlyDictionary<string, object?>? args)
{
var findings = context.Secrets?.Findings ?? [];
if (findings.Count == 0) return false;
var ruleIdPattern = args?.GetValueOrDefault("ruleId") as string;
var severity = args?.GetValueOrDefault("severity") as string;
var confidence = args?.GetValueOrDefault("confidence") as string;
return findings.Any(f =>
MatchesRuleId(f.RuleId, ruleIdPattern) &&
MatchesSeverity(f.Severity, severity) &&
MatchesConfidence(f.Confidence, confidence));
}
private static bool MatchesRuleId(string ruleId, string? pattern)
{
if (string.IsNullOrEmpty(pattern)) return true;
if (pattern.EndsWith("*"))
return ruleId.StartsWith(pattern[..^1], StringComparison.Ordinal);
return ruleId.Equals(pattern, StringComparison.Ordinal);
}
}
```
Location: `src/Policy/__Libraries/StellaOps.Policy/Predicates/Secret/`
### PSD-004: bundle.version Predicate
Implement the `secret.bundle.version()` predicate:
```csharp
/// <summary>
/// Returns true if the active bundle meets or exceeds the required version.
/// </summary>
/// <example>
/// secret.bundle.version("2026.01") // At least 2026.01
/// </example>
[DslPredicate("secret.bundle.version")]
public sealed class SecretBundleVersionPredicate : IPolicyPredicate
{
public bool Evaluate(
PolicyEvaluationContext context,
IReadOnlyDictionary<string, object?>? args)
{
var requiredVersion = args?.GetValueOrDefault("requiredVersion") as string
?? throw new PolicyEvaluationException("secret.bundle.version requires requiredVersion argument");
var bundle = context.Secrets?.Bundle;
if (bundle == null) return false;
return CompareVersions(bundle.Version, requiredVersion) >= 0;
}
private static int CompareVersions(string current, string required)
{
// Simple calendar version comparison (YYYY.MM format)
return string.Compare(current, required, StringComparison.Ordinal);
}
}
```
### PSD-005: match.count Predicate
Implement the `secret.match.count()` predicate:
```csharp
/// <summary>
/// Returns the count of findings matching the filter criteria.
/// </summary>
/// <example>
/// secret.match.count() > 0 // Any findings
/// secret.match.count(ruleId: "stellaops.secrets.aws-*") >= 5 // Many AWS findings
/// </example>
[DslPredicate("secret.match.count")]
public sealed class SecretMatchCountPredicate : IPolicyPredicate<int>
{
public int Evaluate(
PolicyEvaluationContext context,
IReadOnlyDictionary<string, object?>? args)
{
var findings = context.Secrets?.Findings ?? [];
var ruleIdPattern = args?.GetValueOrDefault("ruleId") as string;
return findings.Count(f => MatchesRuleId(f.RuleId, ruleIdPattern));
}
}
```
### PSD-006: mask.applied Predicate
Implement the `secret.mask.applied` predicate:
```csharp
/// <summary>
/// Returns true if masking was successfully applied to all findings.
/// </summary>
/// <example>
/// secret.mask.applied // Verify masking succeeded
/// </example>
[DslPredicate("secret.mask.applied")]
public sealed class SecretMaskAppliedPredicate : IPolicyPredicate
{
public bool Evaluate(
PolicyEvaluationContext context,
IReadOnlyDictionary<string, object?>? args)
{
return context.Secrets?.MaskingApplied ?? true; // Default true if no findings
}
}
```
### PSD-007: path.allowlist Predicate
Implement the `secret.path.allowlist()` predicate for false positive suppression:
```csharp
/// <summary>
/// Returns true if all findings are in paths matching the allowlist patterns.
/// </summary>
/// <example>
/// secret.path.allowlist(["**/test/**", "**/fixtures/**"]) // Ignore test files
/// </example>
[DslPredicate("secret.path.allowlist")]
public sealed class SecretPathAllowlistPredicate : IPolicyPredicate
{
public bool Evaluate(
PolicyEvaluationContext context,
IReadOnlyDictionary<string, object?>? args)
{
var patterns = args?.GetValueOrDefault("patterns") as IReadOnlyList<string>;
if (patterns == null || patterns.Count == 0)
throw new PolicyEvaluationException("secret.path.allowlist requires patterns argument");
var findings = context.Secrets?.Findings ?? [];
if (findings.Count == 0) return true;
return findings.All(f => patterns.Any(p => GlobMatcher.IsMatch(f.FilePath, p)));
}
}
```
### PSD-008: Predicate Registration
Register predicates in the DSL registry:
```csharp
public static class SecretPredicateRegistration
{
public static void RegisterSecretPredicates(this PolicyDslRegistry registry)
{
registry.RegisterPredicate<SecretHasFindingPredicate>("secret.hasFinding");
registry.RegisterPredicate<SecretBundleVersionPredicate>("secret.bundle.version");
registry.RegisterPredicate<SecretMatchCountPredicate>("secret.match.count");
registry.RegisterPredicate<SecretMaskAppliedPredicate>("secret.mask.applied");
registry.RegisterPredicate<SecretPathAllowlistPredicate>("secret.path.allowlist");
}
}
```
### PSD-009: DSL Schema Validation
Update the DSL schema to include secret predicates:
```json
{
"predicates": {
"secret.hasFinding": {
"description": "Returns true if any secret finding matches the filter",
"arguments": {
"ruleId": { "type": "string", "optional": true, "description": "Rule ID pattern (supports * glob)" },
"severity": { "type": "string", "optional": true, "enum": ["low", "medium", "high", "critical"] },
"confidence": { "type": "string", "optional": true, "enum": ["low", "medium", "high"] }
},
"returns": "boolean"
},
"secret.bundle.version": {
"description": "Returns true if the active bundle meets or exceeds the required version",
"arguments": {
"requiredVersion": { "type": "string", "required": true, "description": "Minimum required version (YYYY.MM format)" }
},
"returns": "boolean"
},
"secret.match.count": {
"description": "Returns the count of findings matching the filter",
"arguments": {
"ruleId": { "type": "string", "optional": true, "description": "Rule ID pattern (supports * glob)" }
},
"returns": "integer"
},
"secret.mask.applied": {
"description": "Returns true if masking was successfully applied to all findings",
"arguments": {},
"returns": "boolean"
},
"secret.path.allowlist": {
"description": "Returns true if all findings are in paths matching the allowlist",
"arguments": {
"patterns": { "type": "array", "items": { "type": "string" }, "required": true }
},
"returns": "boolean"
}
}
}
```
### PSD-010: Example Policy Templates
Create example policies in `docs/modules/policy/examples/`:
**secret-blocker.stella** - Block high-severity secrets:
```dsl
policy "Secret Leak Guard" syntax "stella-dsl@1" {
metadata {
description = "Block high-confidence secret leaks in production scans"
tags = ["secrets", "compliance", "security"]
}
rule block_critical priority 100 {
when secret.hasFinding(severity: "critical")
then escalate to "block";
because "Critical secret leak detected - deployment blocked";
}
rule block_high_confidence priority 90 {
when secret.hasFinding(severity: "high", confidence: "high")
then escalate to "block";
because "High severity secret leak with high confidence detected";
}
rule warn_medium priority 50 {
when secret.hasFinding(severity: "medium")
then warn message "Medium severity secret detected - review required";
}
rule require_current_bundle priority 10 {
when not secret.bundle.version("2026.01")
then warn message "Secret detection bundle is out of date";
}
}
```
**secret-allowlist.stella** - Suppress test file findings:
```dsl
policy "Secret Allowlist" syntax "stella-dsl@1" {
metadata {
description = "Suppress false positives in test fixtures"
tags = ["secrets", "testing"]
}
rule allow_test_fixtures priority 200 {
when secret.path.allowlist([
"**/test/**",
"**/tests/**",
"**/fixtures/**",
"**/__fixtures__/**",
"**/testdata/**"
])
then annotate decision.notes := "Findings in test paths - suppressed";
else continue;
}
rule allow_examples priority 190 {
when secret.path.allowlist([
"**/examples/**",
"**/samples/**",
"**/docs/**"
])
and secret.hasFinding(confidence: "low")
then annotate decision.notes := "Low confidence findings in example paths";
else continue;
}
}
```
**secret-threshold.stella** - Threshold-based blocking:
```dsl
policy "Secret Threshold Guard" syntax "stella-dsl@1" {
metadata {
description = "Block scans exceeding secret finding thresholds"
tags = ["secrets", "thresholds"]
}
rule excessive_secrets priority 80 {
when secret.match.count() > 50
then escalate to "block";
because "Excessive number of secret findings (>50) - likely misconfigured scan";
}
rule many_aws_secrets priority 70 {
when secret.match.count(ruleId: "stellaops.secrets.aws-*") > 10
then escalate to "review";
because "Multiple AWS credentials detected - security review required";
}
}
```
### PSD-011: Tests
Unit tests in `src/Policy/__Tests/StellaOps.Policy.Tests/Predicates/Secret/`:
```
├── SecretHasFindingPredicateTests.cs
│ - Test empty findings returns false
│ - Test matching severity filter
│ - Test matching confidence filter
│ - Test ruleId glob matching
│ - Test combined filters
├── SecretBundleVersionPredicateTests.cs
│ - Test version comparison
│ - Test missing bundle returns false
│ - Test exact version match
├── SecretMatchCountPredicateTests.cs
│ - Test empty findings returns 0
│ - Test count with filter
│ - Test count without filter
├── SecretMaskAppliedPredicateTests.cs
│ - Test masking applied
│ - Test masking not applied
│ - Test default for no findings
├── SecretPathAllowlistPredicateTests.cs
│ - Test glob pattern matching
│ - Test multiple patterns
│ - Test no matching patterns
└── PolicyEvaluationIntegrationTests.cs
- Test full policy evaluation with secrets
- Test policy chaining
- Test decision propagation
```
### PSD-012: Documentation
Update `docs/policy/dsl.md` with new predicates section:
```markdown
## Secret Leak Detection Predicates
The following predicates are available for secret leak detection policy rules:
### secret.hasFinding(ruleId?, severity?, confidence?)
Returns `true` if any secret finding matches the specified filters.
**Arguments:**
- `ruleId` (string, optional): Rule ID pattern with optional `*` glob suffix
- `severity` (string, optional): One of `low`, `medium`, `high`, `critical`
- `confidence` (string, optional): One of `low`, `medium`, `high`
**Example:**
```dsl
when secret.hasFinding(severity: "high", confidence: "high")
```
### secret.bundle.version(requiredVersion)
Returns `true` if the active rule bundle version meets or exceeds the required version.
**Arguments:**
- `requiredVersion` (string, required): Minimum version in `YYYY.MM` format
**Example:**
```dsl
when not secret.bundle.version("2026.01")
then warn message "Bundle out of date";
```
### secret.match.count(ruleId?)
Returns the integer count of findings matching the optional rule ID filter.
**Example:**
```dsl
when secret.match.count() > 50
```
### secret.mask.applied
Returns `true` if payload masking was successfully applied to all findings.
**Example:**
```dsl
when not secret.mask.applied
then escalate to "block";
because "Masking failed - secrets may be exposed";
```
### secret.path.allowlist(patterns)
Returns `true` if all findings are in file paths matching at least one allowlist pattern.
**Arguments:**
- `patterns` (array of strings, required): Glob patterns for allowed paths
**Example:**
```dsl
when secret.path.allowlist(["**/test/**", "**/fixtures/**"])
```
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Glob patterns for ruleId | Simple, familiar syntax for rule filtering |
| YYYY.MM version format | Matches bundle versioning convention |
| Default true for mask.applied with no findings | Conservative - don't fail on clean scans |
| Path allowlist as AND | All findings must be in allowed paths to pass |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Part of secret leak detection implementation |
| 2026-01-04 | PSD-001 to PSD-008 completed | Created ISecretEvidenceProvider, SecretFinding, SecretBundleMetadata, SecretEvidenceContext, SecretSignalBinder in `src/Policy/__Libraries/StellaOps.Policy/Secrets/`. Created SecretSignalContextExtensions in PolicyDsl (moved to avoid circular dependency). |
| 2026-01-04 | PSD-009 completed | Created signals-schema@1.json, updated spl-schema@1.json with secret signal examples and new operators (matches, exists). |
| 2026-01-04 | PSD-010 completed | Created spl-secret-block@1.json and spl-secret-warn@1.json example policies. |
| 2026-01-04 | PSD-011 completed | Created unit tests in SecretEvidenceContextTests.cs, SecretSignalBinderTests.cs, SecretSignalContextExtensionsTests.cs. All 8 PolicyDsl tests pass. |
| 2026-01-04 | PSD-012 completed | Updated docs/modules/policy/secret-leak-detection-readiness.md with implemented predicates table and code examples. |
| 2026-01-04 | Sprint completed | All 12 tasks DONE. |

View File

@@ -0,0 +1,604 @@
# Sprint 20260104_005_AIRGAP - Secret Detection Offline Kit Integration
## Topic & Scope
Integrate secret detection rule bundles with the Offline Kit infrastructure for air-gapped deployments. This sprint ensures complete offline parity for secret leak detection.
**Key deliverables:**
1. **Bundle Distribution**: Include signed bundles in Offline Kit exports
2. **Import Workflow**: Bundle import and verification scripts
3. **Attestor Mirror**: Local verification support without internet
4. **CI/CD Integration**: Automated bundle inclusion in releases
5. **Upgrade Path**: Bundle rotation procedures for offline environments
**Working directory:** `src/AirGap/`, `devops/offline/`, `offline/rules/secrets/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_002 (Core Analyzer), Sprint 20260104_003 (Rule Bundles)
- **Parallel with**: Sprint 20260104_004 (Policy DSL)
- **Blocks**: Production deployment of secret leak detection
## Documentation Prerequisites
- docs/24_OFFLINE_KIT.md
- docs/modules/airgap/airgap-mode.md
- docs/modules/scanner/operations/secret-leak-detection.md
- CLAUDE.md Section 8 (Determinism)
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | OKS-001 | DONE | None | AirGap Guild | Update Offline Kit manifest schema for rules |
| 2 | OKS-002 | DONE | OKS-001 | AirGap Guild | Add secrets bundle to BundleBuilder |
| 3 | OKS-003 | DONE | OKS-002 | AirGap Guild | Create bundle verification in Importer |
| 4 | OKS-004 | DONE | None | AirGap Guild | Add Attestor mirror support for bundle verification |
| 5 | OKS-005 | DONE | OKS-003 | AirGap Guild | Create bundle installation script |
| 6 | OKS-006 | DONE | OKS-005 | AirGap Guild | Add bundle rotation/upgrade workflow |
| 7 | OKS-007 | DONE | None | CI/CD Guild | Add bundle to release workflow |
| 8 | OKS-008 | DONE | All | AirGap Guild | Add integration tests for offline flow |
| 9 | OKS-009 | DONE | All | Docs Guild | Update offline kit documentation |
| 10 | OKS-010 | DONE | All | DevOps Guild | Update Helm charts for bundle mounting |
## Task Details
### OKS-001: Manifest Schema Update
Update the Offline Kit manifest to include rule bundles:
```json
{
"version": "2.1",
"created": "2026-01-04T00:00:00Z",
"components": {
"advisory": { ... },
"policy": { ... },
"vex": { ... },
"rules": {
"secrets": {
"bundleId": "secrets.ruleset",
"version": "2026.01",
"path": "rules/secrets/2026.01",
"files": [
{
"name": "secrets.ruleset.manifest.json",
"sha256": "abc123..."
},
{
"name": "secrets.ruleset.rules.jsonl",
"sha256": "def456..."
},
{
"name": "secrets.ruleset.dsse.json",
"sha256": "ghi789..."
}
],
"signature": {
"keyId": "stellaops-secrets-signer",
"verifiedAt": "2026-01-04T00:00:00Z"
}
}
}
}
}
```
Location: `src/AirGap/__Libraries/StellaOps.AirGap.Bundle/Schemas/`
### OKS-002: Bundle Builder Extension
Extend BundleBuilder to include secrets rule bundles:
```csharp
public sealed class SnapshotBundleBuilder
{
// Add secrets bundle extraction
public async Task<BundleResult> BuildAsync(
BundleBuildContext context,
CancellationToken ct = default)
{
// ... existing extractors ...
// Add secrets rules extractor
await ExtractSecretsRulesAsync(context, ct);
return result;
}
private async Task ExtractSecretsRulesAsync(
BundleBuildContext context,
CancellationToken ct)
{
var sourcePath = _options.SecretsRulesBundlePath;
if (string.IsNullOrEmpty(sourcePath) || !Directory.Exists(sourcePath))
{
_logger.LogWarning("Secrets rules bundle not found at {Path}", sourcePath);
return;
}
var targetPath = Path.Combine(context.OutputPath, "rules", "secrets");
Directory.CreateDirectory(targetPath);
// Copy bundle files
foreach (var file in Directory.GetFiles(sourcePath, "secrets.ruleset.*"))
{
var targetFile = Path.Combine(targetPath, Path.GetFileName(file));
await CopyWithIntegrityAsync(file, targetFile, ct);
}
// Add to manifest
context.Manifest.Rules["secrets"] = new RuleBundleManifest
{
BundleId = "secrets.ruleset",
Version = await ReadBundleVersionAsync(sourcePath, ct),
Path = "rules/secrets"
};
}
}
```
Location: `src/AirGap/__Libraries/StellaOps.AirGap.Bundle/Services/`
### OKS-003: Importer Verification
Add bundle verification to the Offline Kit importer:
```csharp
public sealed class OfflineKitImporter
{
public async Task<ImportResult> ImportAsync(
string kitPath,
ImportOptions options,
CancellationToken ct = default)
{
// ... existing imports ...
// Import and verify secrets rules
if (manifest.Rules.TryGetValue("secrets", out var secretsBundle))
{
await ImportSecretsRulesAsync(kitPath, secretsBundle, options, ct);
}
return result;
}
private async Task ImportSecretsRulesAsync(
string kitPath,
RuleBundleManifest bundle,
ImportOptions options,
CancellationToken ct)
{
var sourcePath = Path.Combine(kitPath, bundle.Path);
var targetPath = _options.SecretsRulesInstallPath;
// Verify bundle integrity
var verifier = _serviceProvider.GetRequiredService<IBundleVerifier>();
var verification = await verifier.VerifyAsync(sourcePath, new VerificationOptions
{
AttestorUrl = options.AttestorMirrorUrl,
RequireRekorProof = options.RequireRekorProof
}, ct);
if (!verification.IsValid)
{
throw new ImportException($"Secrets bundle verification failed: {string.Join(", ", verification.ValidationErrors ?? [])}");
}
// Install bundle
await InstallBundleAsync(sourcePath, targetPath, ct);
_logger.LogInformation(
"Installed secrets bundle {Version} signed by {KeyId}",
verification.BundleVersion,
verification.SignerKeyId);
}
}
```
### OKS-004: Attestor Mirror Support
Configure Attestor mirror for offline bundle verification:
```csharp
public sealed class OfflineAttestorClient : IAttestorClient
{
private readonly string _mirrorPath;
public OfflineAttestorClient(string mirrorPath)
{
_mirrorPath = mirrorPath;
}
public async Task<VerificationResult> VerifyDsseAsync(
DsseEnvelope envelope,
VerifyOptions options,
CancellationToken ct = default)
{
// Load mirrored certificate chain
var chainPath = Path.Combine(_mirrorPath, "certs", options.KeyId + ".pem");
if (!File.Exists(chainPath))
{
return VerificationResult.Failed($"Certificate not found in mirror: {options.KeyId}");
}
var chain = await LoadCertificateChainAsync(chainPath, ct);
// Verify signature locally
var result = await _dsseVerifier.VerifyAsync(envelope, chain, ct);
// Optionally verify against mirrored Rekor entries
if (options.RequireRekorProof)
{
var rekorPath = Path.Combine(_mirrorPath, "rekor", envelope.PayloadDigest + ".json");
if (!File.Exists(rekorPath))
{
return VerificationResult.Failed("Rekor entry not found in mirror");
}
result = result.WithRekorEntry(await LoadRekorEntryAsync(rekorPath, ct));
}
return result;
}
}
```
### OKS-005: Installation Script
Create bundle installation script for operators:
**`devops/offline/scripts/install-secrets-bundle.sh`:**
```bash
#!/bin/bash
set -euo pipefail
BUNDLE_PATH="${1:-/mnt/offline-kit/rules/secrets}"
INSTALL_PATH="${2:-/opt/stellaops/plugins/scanner/analyzers/secrets}"
ATTESTOR_MIRROR="${3:-/mnt/offline-kit/attestor-mirror}"
echo "Installing secrets bundle from ${BUNDLE_PATH}"
# Verify bundle before installation
export STELLA_ATTESTOR_URL="file://${ATTESTOR_MIRROR}"
if ! stella secrets bundle verify --bundle "${BUNDLE_PATH}" --require-rekor; then
echo "ERROR: Bundle verification failed" >&2
exit 1
fi
# Create installation directory
mkdir -p "${INSTALL_PATH}"
# Install bundle files
cp -v "${BUNDLE_PATH}"/secrets.ruleset.* "${INSTALL_PATH}/"
# Set permissions
chmod 640 "${INSTALL_PATH}"/secrets.ruleset.*
chown stellaops:stellaops "${INSTALL_PATH}"/secrets.ruleset.*
# Verify installation
INSTALLED_VERSION=$(jq -r '.version' "${INSTALL_PATH}/secrets.ruleset.manifest.json")
echo "Successfully installed secrets bundle version ${INSTALLED_VERSION}"
echo ""
echo "To activate, restart Scanner Worker:"
echo " systemctl restart stellaops-scanner-worker"
echo ""
echo "Or with Kubernetes:"
echo " kubectl rollout restart deployment/scanner-worker"
```
### OKS-006: Bundle Rotation Workflow
Document and implement bundle upgrade procedure:
**Upgrade Workflow:**
1. **Pre-upgrade Verification**
```bash
# Verify new bundle
stella secrets bundle verify --bundle /path/to/new-bundle
# Compare with current
CURRENT=$(jq -r '.version' /opt/stellaops/.../secrets.ruleset.manifest.json)
NEW=$(jq -r '.version' /path/to/new-bundle/secrets.ruleset.manifest.json)
echo "Upgrading from ${CURRENT} to ${NEW}"
```
2. **Backup Current Bundle**
```bash
BACKUP_DIR="/opt/stellaops/backups/secrets-bundles/$(date +%Y%m%d)"
mkdir -p "${BACKUP_DIR}"
cp -a /opt/stellaops/plugins/scanner/analyzers/secrets/* "${BACKUP_DIR}/"
```
3. **Install New Bundle**
```bash
./install-secrets-bundle.sh /path/to/new-bundle
```
4. **Rolling Restart**
```bash
# Kubernetes
kubectl rollout restart deployment/scanner-worker --namespace stellaops
# Systemd
systemctl restart stellaops-scanner-worker
```
5. **Verify Upgrade**
```bash
# Check logs for new version
kubectl logs -l app=scanner-worker --tail=100 | grep "SecretsAnalyzerHost"
```
**Rollback Procedure:**
```bash
# Restore backup
cp -a "${BACKUP_DIR}"/* /opt/stellaops/plugins/scanner/analyzers/secrets/
# Restart workers
kubectl rollout restart deployment/scanner-worker
```
### OKS-007: Release Workflow Integration
Add secrets bundle to CI/CD release pipeline:
**`.gitea/workflows/release-offline-kit.yml`:**
```yaml
jobs:
build-offline-kit:
steps:
- name: Build secrets bundle
run: |
stella secrets bundle create \
--output ./offline-kit/rules/secrets/${VERSION} \
--bundle-id secrets.ruleset \
--version ${VERSION} \
--rules ./offline/rules/secrets/sources/*.json \
--sign \
--key-id ${SECRETS_SIGNER_KEY_ID}
- name: Include in offline kit
run: |
# Bundle is automatically included via BundleBuilder
- name: Verify bundle in kit
run: |
stella secrets bundle verify \
--bundle ./offline-kit/rules/secrets/${VERSION}
```
**Add to `.gitea/scripts/build/build-offline-kit.sh`:**
```bash
# Build and sign secrets bundle
echo "Building secrets rule bundle..."
stella secrets bundle create \
--output "${OUTPUT_DIR}/rules/secrets/${BUNDLE_VERSION}" \
--bundle-id secrets.ruleset \
--version "${BUNDLE_VERSION}" \
--rules offline/rules/secrets/sources/*.json \
--sign \
--key-id "${SECRETS_SIGNER_KEY_ID}"
```
### OKS-008: Integration Tests
Create integration tests for offline flow:
```csharp
[Trait("Category", "Integration")]
public class OfflineSecretsIntegrationTests : IClassFixture<OfflineKitFixture>
{
[Fact]
public async Task OfflineKit_IncludesSecretsBundleWithValidSignature()
{
// Arrange
var kit = await _fixture.BuildOfflineKitAsync();
// Act
var bundlePath = Path.Combine(kit.Path, "rules", "secrets");
var verifier = new BundleVerifier(_attestorMirror);
var result = await verifier.VerifyAsync(bundlePath, new VerificationOptions());
// Assert
result.IsValid.Should().BeTrue();
result.BundleVersion.Should().NotBeEmpty();
}
[Fact]
public async Task Importer_InstallsAndVerifiesBundle()
{
// Arrange
var kit = await _fixture.BuildOfflineKitAsync();
var importer = new OfflineKitImporter(_options);
// Act
var result = await importer.ImportAsync(kit.Path, new ImportOptions
{
AttestorMirrorUrl = _attestorMirrorUrl
});
// Assert
result.Success.Should().BeTrue();
Directory.Exists(_installPath).Should().BeTrue();
File.Exists(Path.Combine(_installPath, "secrets.ruleset.manifest.json")).Should().BeTrue();
}
[Fact]
public async Task Scanner_LoadsBundleFromOfflineInstallation()
{
// Arrange
await ImportOfflineKitAsync();
// Act
var host = new SecretsAnalyzerHost(_options, _logger);
await host.StartAsync(CancellationToken.None);
// Assert
host.IsEnabled.Should().BeTrue();
host.BundleVersion.Should().Be(_expectedVersion);
}
}
```
### OKS-009: Documentation Updates
Update `docs/24_OFFLINE_KIT.md`:
```markdown
## Secret Detection Rules
The Offline Kit includes DSSE-signed rule bundles for secret leak detection.
### Bundle Contents
```
offline-kit/
├── rules/
│ └── secrets/
│ └── 2026.01/
│ ├── secrets.ruleset.manifest.json # Rule metadata
│ ├── secrets.ruleset.rules.jsonl # Rule definitions
│ └── secrets.ruleset.dsse.json # DSSE signature
└── attestor-mirror/
├── certs/
│ └── stellaops-secrets-signer.pem # Signing certificate
└── rekor/
└── <digest>.json # Transparency log entry
```
### Installation
1. **Verify Bundle**
```bash
export STELLA_ATTESTOR_URL="file:///mnt/offline-kit/attestor-mirror"
stella secrets bundle verify --bundle /mnt/offline-kit/rules/secrets/2026.01
```
2. **Install Bundle**
```bash
./devops/offline/scripts/install-secrets-bundle.sh \
/mnt/offline-kit/rules/secrets/2026.01 \
/opt/stellaops/plugins/scanner/analyzers/secrets
```
3. **Enable Feature**
```yaml
scanner:
features:
experimental:
secret-leak-detection: true
```
4. **Restart Workers**
```bash
kubectl rollout restart deployment/scanner-worker
```
### Verification
Check that the bundle is loaded:
```bash
kubectl logs -l app=scanner-worker --tail=100 | grep SecretsAnalyzerHost
# Expected: SecretsAnalyzerHost: Loaded bundle 2026.01 signed by stellaops-secrets-signer with N rules
```
### Bundle Rotation
See [Secret Bundle Rotation](./modules/scanner/operations/secrets-bundle-rotation.md) for upgrade procedures.
```
### OKS-010: Helm Chart Updates
Update Helm charts for bundle mounting:
**`devops/helm/stellaops/templates/scanner-worker-deployment.yaml`:**
```yaml
spec:
template:
spec:
volumes:
# Existing volumes...
- name: secrets-rules
{{- if .Values.scanner.secretsRules.persistentVolumeClaim }}
persistentVolumeClaim:
claimName: {{ .Values.scanner.secretsRules.persistentVolumeClaim }}
{{- else }}
configMap:
name: {{ include "stellaops.fullname" . }}-secrets-rules
{{- end }}
containers:
- name: scanner-worker
volumeMounts:
# Existing mounts...
- name: secrets-rules
mountPath: /opt/stellaops/plugins/scanner/analyzers/secrets
readOnly: true
```
**`devops/helm/stellaops/values.yaml`:**
```yaml
scanner:
features:
experimental:
secretLeakDetection: false # Enable via override
secretsRules:
# Use PVC for air-gapped installations
persistentVolumeClaim: ""
# Or use ConfigMap for simple deployments
bundleVersion: "2026.01"
```
## Directory Structure
```
offline/rules/secrets/
├── sources/ # Source rule JSON files (not in kit)
│ ├── aws-access-key.json
│ └── ...
├── 2026.01/ # Signed bundle (in kit)
│ ├── secrets.ruleset.manifest.json
│ ├── secrets.ruleset.rules.jsonl
│ └── secrets.ruleset.dsse.json
└── latest -> 2026.01 # Symlink
devops/offline/
├── scripts/
│ ├── install-secrets-bundle.sh # Installation script
│ └── rotate-secrets-bundle.sh # Rotation script
└── templates/
└── secrets-bundle-pvc.yaml # PVC template for air-gap
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Include Attestor mirror in kit | Enables fully offline verification |
| File:// URL for offline Attestor | Simple, no network required |
| ConfigMap fallback | Simpler for non-air-gapped deployments |
| Symlink for latest | Atomic version switching |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Part of secret leak detection implementation |
| 2026-01-04 | OKS-001 DONE | Added RuleBundleComponent to OfflineKitManifest.cs with rules schema |
| 2026-01-04 | OKS-002 DONE | Extended SnapshotBundleWriter/Reader, added RulesSnapshotExtractor |
| 2026-01-04 | OKS-003 DONE | Created RuleBundleValidator with digest/signature/monotonicity checks |
| 2026-01-04 | OKS-004 DONE | Added RuleBundleSigningPath to FileSystemRootStore, DsseVerifier support |
| 2026-01-04 | OKS-005 DONE | Created devops/offline/scripts/install-secrets-bundle.sh |
| 2026-01-04 | OKS-006 DONE | Created devops/offline/scripts/rotate-secrets-bundle.sh with rollback |
| 2026-01-04 | OKS-007 DONE | Created .gitea/workflows/secrets-bundle-release.yml CI/CD workflow |
| 2026-01-04 | OKS-008 DONE | Added RuleBundleValidatorTests.cs with 8 test cases |
| 2026-01-04 | OKS-009 DONE | Updated docs/24_OFFLINE_KIT.md with secrets bundle documentation |
| 2026-01-04 | OKS-010 DONE | Updated values-airgap.yaml with secrets-rules volume mount and PVC |
| 2026-01-04 | Fix build errors | Fixed 4 nullability errors in OfflineVerificationPolicy.cs, JsonNormalizer.cs, SbomNormalizer.cs |
| 2026-01-04 | Fix test versions | Updated RuleBundleValidatorTests to use 3-part semver (2026.1.0) instead of CalVer |
| 2026-01-04 | Sprint complete | All 10 tasks completed, build passes, tests pass (9/9) |

View File

@@ -0,0 +1,216 @@
# Sprint 20260104_006_BE - Secret Detection Configuration API
## Topic & Scope
Backend APIs and data models for configuring secret detection behavior per tenant. This sprint provides the foundation for UI configuration of secret leak detection.
**Key deliverables:**
1. **Tenant Settings Model**: Per-tenant secret detection configuration
2. **Revelation Policy**: Control how detected secrets are displayed/masked
3. **Exception Management**: Allowlist patterns for false positives
4. **Configuration API**: CRUD endpoints for settings
**Working directory:** `src/Scanner/`, `src/Platform/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_001 (Core Analyzer), Sprint 20260104_002 (Rule Bundles)
- **Parallel with**: Sprint 20260104_007 (Alert Integration)
- **Blocks**: Sprint 20260104_008 (UI)
## Documentation Prerequisites
- docs/modules/scanner/operations/secret-leak-detection.md
- CLAUDE.md Section 8 (Determinism)
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | SDC-001 | DONE | None | Scanner Guild | Define SecretDetectionSettings domain model |
| 2 | SDC-002 | DONE | SDC-001 | Scanner Guild | Create SecretRevelationPolicy enum and config |
| 3 | SDC-003 | DONE | SDC-001 | Scanner Guild | Create SecretExceptionPattern model for allowlists |
| 4 | SDC-004 | DONE | SDC-001 | Platform Guild | Add persistence (Dapper migrations) |
| 5 | SDC-005 | DONE | SDC-004 | Platform Guild | Create Settings CRUD API endpoints |
| 6 | SDC-006 | DONE | SDC-005 | Platform Guild | Add OpenAPI spec for settings endpoints |
| 7 | SDC-007 | DONE | SDC-003 | Scanner Guild | Integrate exception patterns into SecretsAnalyzerHost |
| 8 | SDC-008 | DONE | SDC-002 | Scanner Guild | Implement revelation policy in findings output |
| 9 | SDC-009 | DONE | All | Scanner Guild | Add unit and integration tests |
## Task Details
### SDC-001: SecretDetectionSettings Domain Model
```csharp
public sealed record SecretDetectionSettings
{
public required Guid TenantId { get; init; }
public required bool Enabled { get; init; }
public required SecretRevelationPolicy RevelationPolicy { get; init; }
public required IReadOnlyList<string> EnabledRuleCategories { get; init; }
public required IReadOnlyList<SecretExceptionPattern> Exceptions { get; init; }
public required SecretAlertSettings AlertSettings { get; init; }
public required DateTimeOffset UpdatedAt { get; init; }
public required string UpdatedBy { get; init; }
}
```
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Core/Secrets/Configuration/`
### SDC-002: SecretRevelationPolicy
Control how detected secrets appear in different contexts:
```csharp
public enum SecretRevelationPolicy
{
/// <summary>
/// Show only that a secret was detected, no value shown.
/// Example: [SECRET_DETECTED: aws_access_key_id]
/// </summary>
FullMask = 0,
/// <summary>
/// Show first and last 4 characters.
/// Example: AKIA****WXYZ
/// </summary>
PartialReveal = 1,
/// <summary>
/// Show full value (requires elevated permissions).
/// Use only for debugging/incident response.
/// </summary>
FullReveal = 2
}
public sealed record RevelationPolicyConfig
{
/// <summary>Default policy for UI/API responses.</summary>
public SecretRevelationPolicy DefaultPolicy { get; init; } = SecretRevelationPolicy.PartialReveal;
/// <summary>Policy for exported reports (PDF, JSON).</summary>
public SecretRevelationPolicy ExportPolicy { get; init; } = SecretRevelationPolicy.FullMask;
/// <summary>Policy for logs and telemetry.</summary>
public SecretRevelationPolicy LogPolicy { get; init; } = SecretRevelationPolicy.FullMask;
/// <summary>Roles allowed to use FullReveal.</summary>
public IReadOnlyList<string> FullRevealRoles { get; init; } = ["security-admin", "incident-responder"];
/// <summary>Number of characters to show at start/end for PartialReveal.</summary>
public int PartialRevealChars { get; init; } = 4;
}
```
### SDC-003: SecretExceptionPattern (Allowlist)
```csharp
public sealed record SecretExceptionPattern
{
public required Guid Id { get; init; }
public required string Name { get; init; }
public required string Description { get; init; }
/// <summary>Regex pattern to match against detected secret value.</summary>
public required string Pattern { get; init; }
/// <summary>Optional: Only apply to specific rule IDs.</summary>
public IReadOnlyList<string>? ApplicableRuleIds { get; init; }
/// <summary>Optional: Only apply to specific file paths.</summary>
public string? FilePathGlob { get; init; }
/// <summary>Reason for exception (audit trail).</summary>
public required string Justification { get; init; }
/// <summary>Expiration date (null = permanent).</summary>
public DateTimeOffset? ExpiresAt { get; init; }
public required DateTimeOffset CreatedAt { get; init; }
public required string CreatedBy { get; init; }
}
```
### SDC-005: Settings API Endpoints
```
GET /api/v1/tenants/{tenantId}/settings/secret-detection
PUT /api/v1/tenants/{tenantId}/settings/secret-detection
PATCH /api/v1/tenants/{tenantId}/settings/secret-detection
GET /api/v1/tenants/{tenantId}/settings/secret-detection/exceptions
POST /api/v1/tenants/{tenantId}/settings/secret-detection/exceptions
DELETE /api/v1/tenants/{tenantId}/settings/secret-detection/exceptions/{exceptionId}
GET /api/v1/tenants/{tenantId}/settings/secret-detection/rule-categories
```
### SDC-008: Revelation Policy Implementation
```csharp
public static class SecretMasker
{
public static string Mask(string secretValue, SecretRevelationPolicy policy, int partialChars = 4)
{
return policy switch
{
SecretRevelationPolicy.FullMask => "[REDACTED]",
SecretRevelationPolicy.PartialReveal => MaskPartial(secretValue, partialChars),
SecretRevelationPolicy.FullReveal => secretValue,
_ => "[REDACTED]"
};
}
private static string MaskPartial(string value, int chars)
{
if (value.Length <= chars * 2)
return new string('*', value.Length);
var prefix = value[..chars];
var suffix = value[^chars..];
var masked = new string('*', Math.Min(value.Length - chars * 2, 8));
return $"{prefix}{masked}{suffix}";
}
}
```
## Directory Structure
```
src/Scanner/__Libraries/StellaOps.Scanner.Core/
├── Secrets/
│ ├── Configuration/
│ │ ├── SecretDetectionSettings.cs
│ │ ├── SecretRevelationPolicy.cs
│ │ ├── RevelationPolicyConfig.cs
│ │ ├── SecretExceptionPattern.cs
│ │ └── SecretAlertSettings.cs
│ └── Masking/
│ └── SecretMasker.cs
src/Platform/StellaOps.Platform.WebService/
├── Endpoints/
│ └── SecretDetectionSettingsEndpoints.cs
└── Persistence/
└── Migrations/
└── AddSecretDetectionSettings.cs
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Per-tenant settings | Multi-tenant isolation requirement |
| Role-based full reveal | Security: prevent accidental exposure |
| Exception expiration | Force periodic review of allowlists |
| Separate export/log policies | Defense in depth for sensitive data |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Gap identified in secret detection feature |
| 2026-01-04 | SDC-001 to SDC-008 DONE | Domain models, persistence, API endpoints, exception matcher, masker implemented |
| 2026-01-04 | Files created | SecretDetectionSettings.cs, SecretRevelationPolicy.cs, SecretExceptionPattern.cs, SecretAlertSettings.cs, SecretMasker.cs, SecretExceptionMatcher.cs, migration 021_secret_detection_settings.sql, SecretDetectionSettingsRow.cs, ISecretDetectionSettingsRepository.cs, PostgresSecretDetectionSettingsRepository.cs, SecretDetectionConfigContracts.cs, SecretDetectionSettingsService.cs, SecretDetectionSettingsEndpoints.cs |
| 2026-01-04 | SDC-009 DONE | Unit tests created: SecretDetectionSettingsTests.cs, SecretMaskerTests.cs, SecretExceptionPatternTests.cs, SecretExceptionMatcherTests.cs - build verified |

View File

@@ -0,0 +1,298 @@
# Sprint 20260104_007_BE - Secret Detection Alert Integration
## Topic & Scope
Integration between secret detection findings and the Notify service for real-time alerting when secrets are discovered in scans.
**Key deliverables:**
1. **Alert Routing**: Route secret findings to configured channels
2. **Alert Templates**: Formatted notifications for different channels
3. **Rate Limiting**: Prevent alert fatigue from mass findings
4. **Severity Mapping**: Map rule severity to alert priority
**Working directory:** `src/Scanner/`, `src/Notify/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_001 (Core Analyzer), Sprint 20260104_006 (Config API)
- **Parallel with**: Sprint 20260104_008 (UI)
- **Blocks**: Production deployment with alerting
## Documentation Prerequisites
- docs/modules/notify/architecture.md
- docs/modules/scanner/operations/secret-leak-detection.md
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | SDA-001 | DONE | None | Scanner Guild | Define SecretAlertSettings model |
| 2 | SDA-002 | DONE | SDA-001 | Scanner Guild | Create SecretFindingAlertEvent |
| 3 | SDA-003 | DONE | SDA-002 | Notify Guild | Add secret-finding alert template |
| 4 | SDA-004 | DONE | SDA-003 | Notify Guild | Implement Slack/Teams formatters |
| 5 | SDA-005 | DONE | SDA-002 | Scanner Guild | Add alert emission to SecretsAnalyzerHost |
| 6 | SDA-006 | DONE | SDA-005 | Scanner Guild | Implement rate limiting / deduplication |
| 7 | SDA-007 | DONE | SDA-006 | Scanner Guild | Add severity-based routing |
| 8 | SDA-008 | DONE | SDA-001 | Platform Guild | Add alert settings to config API |
| 9 | SDA-009 | DONE | All | Scanner Guild | Add integration tests |
## Task Details
### SDA-001: SecretAlertSettings Model
```csharp
public sealed record SecretAlertSettings
{
/// <summary>Enable/disable alerting for this tenant.</summary>
public bool Enabled { get; init; } = true;
/// <summary>Minimum severity to trigger alert (Critical, High, Medium, Low).</summary>
public SecretSeverity MinimumAlertSeverity { get; init; } = SecretSeverity.High;
/// <summary>Alert destinations by channel type.</summary>
public IReadOnlyList<SecretAlertDestination> Destinations { get; init; } = [];
/// <summary>Rate limit: max alerts per scan.</summary>
public int MaxAlertsPerScan { get; init; } = 10;
/// <summary>Deduplication window: don't re-alert same secret within this period.</summary>
public TimeSpan DeduplicationWindow { get; init; } = TimeSpan.FromHours(24);
/// <summary>Include file path in alert (may reveal repo structure).</summary>
public bool IncludeFilePath { get; init; } = true;
/// <summary>Include masked secret value in alert.</summary>
public bool IncludeMaskedValue { get; init; } = true;
}
public sealed record SecretAlertDestination
{
public required Guid Id { get; init; }
public required AlertChannelType ChannelType { get; init; }
public required string ChannelId { get; init; } // Slack channel ID, email, webhook URL
public IReadOnlyList<SecretSeverity>? SeverityFilter { get; init; }
public IReadOnlyList<string>? RuleCategoryFilter { get; init; }
}
public enum AlertChannelType
{
Slack,
Teams,
Email,
Webhook,
PagerDuty
}
```
### SDA-002: SecretFindingAlertEvent
```csharp
public sealed record SecretFindingAlertEvent
{
public required Guid EventId { get; init; }
public required Guid TenantId { get; init; }
public required Guid ScanId { get; init; }
public required string ImageRef { get; init; }
public required SecretSeverity Severity { get; init; }
public required string RuleId { get; init; }
public required string RuleName { get; init; }
public required string RuleCategory { get; init; }
public required string FilePath { get; init; }
public required int LineNumber { get; init; }
public required string MaskedValue { get; init; }
public required DateTimeOffset DetectedAt { get; init; }
public required string ScanTriggeredBy { get; init; }
/// <summary>Deduplication key for rate limiting.</summary>
public string DeduplicationKey => $"{TenantId}:{RuleId}:{FilePath}:{LineNumber}";
}
```
### SDA-003: Alert Templates
**Slack Template:**
```json
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🚨 Secret Detected in Container Scan"
}
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Severity:*\n{{severity}}" },
{ "type": "mrkdwn", "text": "*Rule:*\n{{ruleName}}" },
{ "type": "mrkdwn", "text": "*Image:*\n`{{imageRef}}`" },
{ "type": "mrkdwn", "text": "*File:*\n`{{filePath}}:{{lineNumber}}`" }
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Detected Value:*\n```{{maskedValue}}```"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View in StellaOps" },
"url": "{{findingUrl}}"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Add Exception" },
"url": "{{exceptionUrl}}"
}
]
}
]
}
```
### SDA-005: Alert Emission in SecretsAnalyzerHost
```csharp
public sealed class SecretsAnalyzerHost
{
private readonly ISecretAlertEmitter _alertEmitter;
private readonly ISecretAlertDeduplicator _deduplicator;
public async Task OnSecretFoundAsync(
SecretFinding finding,
ScanContext context,
CancellationToken ct)
{
var settings = await _settingsProvider.GetAlertSettingsAsync(context.TenantId, ct);
if (!settings.Enabled)
return;
if (finding.Severity < settings.MinimumAlertSeverity)
return;
var alertEvent = MapToAlertEvent(finding, context);
// Check deduplication
if (await _deduplicator.IsDuplicateAsync(alertEvent, settings.DeduplicationWindow, ct))
{
_logger.LogDebug("secret.alert.deduplicated key={key}", alertEvent.DeduplicationKey);
return;
}
// Rate limiting
var alertCount = await _alertEmitter.GetAlertCountForScanAsync(context.ScanId, ct);
if (alertCount >= settings.MaxAlertsPerScan)
{
_logger.LogWarning("secret.alert.rate_limited scan_id={scan_id} count={count}",
context.ScanId, alertCount);
return;
}
// Emit to configured destinations
await _alertEmitter.EmitAsync(alertEvent, settings.Destinations, ct);
}
}
```
### SDA-006: Rate Limiting & Deduplication
```csharp
public interface ISecretAlertDeduplicator
{
Task<bool> IsDuplicateAsync(
SecretFindingAlertEvent alert,
TimeSpan window,
CancellationToken ct);
Task RecordAlertAsync(
SecretFindingAlertEvent alert,
CancellationToken ct);
}
public sealed class ValkeySecretAlertDeduplicator : ISecretAlertDeduplicator
{
private readonly IValkeyConnection _valkey;
public async Task<bool> IsDuplicateAsync(
SecretFindingAlertEvent alert,
TimeSpan window,
CancellationToken ct)
{
var key = $"secret:alert:dedup:{alert.DeduplicationKey}";
var exists = await _valkey.ExistsAsync(key);
return exists;
}
public async Task RecordAlertAsync(
SecretFindingAlertEvent alert,
CancellationToken ct)
{
var key = $"secret:alert:dedup:{alert.DeduplicationKey}";
await _valkey.SetAsync(key, alert.EventId.ToString(), expiry: TimeSpan.FromHours(24));
}
}
```
## Severity Mapping
| Rule Severity | Alert Priority | Default Behavior |
|---------------|----------------|------------------|
| Critical | P1 / Immediate | Always alert, page on-call |
| High | P2 / Urgent | Alert to security channel |
| Medium | P3 / Normal | Alert if configured |
| Low | P4 / Info | No alert by default |
## Directory Structure
```
src/Scanner/__Libraries/StellaOps.Scanner.Core/
├── Secrets/
│ ├── Alerts/
│ │ ├── SecretAlertSettings.cs
│ │ ├── SecretFindingAlertEvent.cs
│ │ ├── ISecretAlertEmitter.cs
│ │ ├── ISecretAlertDeduplicator.cs
│ │ └── ValkeySecretAlertDeduplicator.cs
src/Notify/__Libraries/StellaOps.Notify.Engine/
├── Templates/
│ └── SecretFindingAlertTemplate.cs
├── Formatters/
│ ├── SlackSecretAlertFormatter.cs
│ └── TeamsSecretAlertFormatter.cs
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Valkey for deduplication | Fast, distributed, TTL support |
| Per-scan rate limit | Prevent alert storms on large findings |
| Masked values in alerts | Balance security awareness vs exposure |
| Severity-based routing | Different channels for different priorities |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | Alert integration for secret detection |
| 2026-01-04 | SDA-001 DONE | SecretAlertSettings already implemented in Sprint 006 (SecretAlertSettings.cs) |
| 2026-01-04 | SDA-008 DONE | Alert settings already included in SecretDetectionSettings config API |
| 2026-01-04 | SDA-002 DONE | Created SecretFindingAlertEvent.cs and SecretFindingInfo.cs |
| 2026-01-04 | SDA-005 DONE | Created ISecretAlertEmitter.cs and SecretAlertEmitter.cs |
| 2026-01-04 | SDA-006 DONE | Created ISecretAlertDeduplicator.cs interface |
| 2026-01-04 | SDA-007 DONE | Created ISecretAlertRouter.cs and SecretAlertRouter.cs |
| 2026-01-04 | SDA-003/004 DONE | Created SecretFindingAlertTemplates.cs with Slack, Teams, Email, Webhook, PagerDuty templates |
| 2026-01-04 | SDA-009 DONE | Unit tests: SecretFindingAlertEventTests, SecretAlertRouterTests, SecretAlertEmitterTests |

View File

@@ -0,0 +1,503 @@
# Sprint 20260104_008_FE - Secret Detection UI
## Topic & Scope
Frontend components for configuring and viewing secret detection findings. Provides tenant administrators with tools to manage detection settings, view findings, and configure alerts.
**Key deliverables:**
1. **Settings Page**: Configure secret detection for tenant
2. **Findings Viewer**: View detected secrets with proper masking
3. **Exception Manager**: Add/remove allowlist patterns
4. **Alert Configuration**: Set up notification channels
**Working directory:** `src/Web/StellaOps.Web/`
## Dependencies & Concurrency
- **Depends on**: Sprint 20260104_006 (Config API), Sprint 20260104_007 (Alerts)
- **Parallel with**: None (final UI sprint)
- **Blocks**: Feature release
## Documentation Prerequisites
- docs/modules/web/architecture.md
- Angular v17 component patterns
## Delivery Tracker
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
| --- | --- | --- | --- | --- | --- |
| 1 | SDU-001 | DONE | None | Frontend Guild | Create secret-detection feature module |
| 2 | SDU-002 | DONE | SDU-001 | Frontend Guild | Build settings page component |
| 3 | SDU-003 | DONE | SDU-002 | Frontend Guild | Add revelation policy selector |
| 4 | SDU-004 | DONE | SDU-002 | Frontend Guild | Build rule category toggles |
| 5 | SDU-005 | DONE | SDU-001 | Frontend Guild | Create findings list component |
| 6 | SDU-006 | DONE | SDU-005 | Frontend Guild | Implement masked value display |
| 7 | SDU-007 | DONE | SDU-005 | Frontend Guild | Add finding detail drawer (via exception-manager) |
| 8 | SDU-008 | DONE | SDU-001 | Frontend Guild | Build exception manager component |
| 9 | SDU-009 | DONE | SDU-008 | Frontend Guild | Create exception form with validation |
| 10 | SDU-010 | DONE | SDU-001 | Frontend Guild | Build alert destination config |
| 11 | SDU-011 | DONE | SDU-010 | Frontend Guild | Add channel test functionality |
| 12 | SDU-012 | DONE | All | Frontend Guild | Add E2E tests |
## Task Details
### SDU-002: Settings Page Component
```typescript
// secret-detection-settings.component.ts
@Component({
selector: 'app-secret-detection-settings',
template: `
<div class="settings-container">
<header class="settings-header">
<h1>Secret Detection</h1>
<mat-slide-toggle
[checked]="settings()?.enabled"
(change)="onEnabledChange($event)"
color="primary">
{{ settings()?.enabled ? 'Enabled' : 'Disabled' }}
</mat-slide-toggle>
</header>
<mat-tab-group>
<mat-tab label="General">
<app-revelation-policy-config
[policy]="settings()?.revelationPolicy"
(policyChange)="onPolicyChange($event)" />
<app-rule-category-selector
[categories]="availableCategories()"
[selected]="settings()?.enabledRuleCategories"
(selectionChange)="onCategoriesChange($event)" />
</mat-tab>
<mat-tab label="Exceptions">
<app-exception-manager
[exceptions]="settings()?.exceptions"
(add)="onAddException($event)"
(remove)="onRemoveException($event)" />
</mat-tab>
<mat-tab label="Alerts">
<app-alert-destination-config
[settings]="settings()?.alertSettings"
(settingsChange)="onAlertSettingsChange($event)" />
</mat-tab>
</mat-tab-group>
</div>
`
})
export class SecretDetectionSettingsComponent {
private settingsService = inject(SecretDetectionSettingsService);
settings = this.settingsService.settings;
availableCategories = this.settingsService.availableCategories;
// ... handlers
}
```
### SDU-003: Revelation Policy Selector
```typescript
// revelation-policy-config.component.ts
@Component({
selector: 'app-revelation-policy-config',
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Secret Revelation Policy</mat-card-title>
<mat-card-subtitle>
Control how detected secrets are displayed
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-radio-group
[value]="policy()?.defaultPolicy"
(change)="onDefaultPolicyChange($event)">
<mat-radio-button value="FullMask">
<div class="policy-option">
<strong>Full Mask</strong>
<span class="example">[REDACTED]</span>
<p>No secret value shown. Safest option.</p>
</div>
</mat-radio-button>
<mat-radio-button value="PartialReveal">
<div class="policy-option">
<strong>Partial Reveal</strong>
<span class="example">AKIA****WXYZ</span>
<p>Show first/last 4 characters. Helps identify specific secrets.</p>
</div>
</mat-radio-button>
<mat-radio-button value="FullReveal" [disabled]="!canFullReveal()">
<div class="policy-option">
<strong>Full Reveal</strong>
<span class="example">AKIAIOSFODNN7EXAMPLE</span>
<p>Show complete value. Requires security-admin role.</p>
</div>
</mat-radio-button>
</mat-radio-group>
<mat-divider />
<h4>Context-Specific Policies</h4>
<div class="context-policies">
<mat-form-field>
<mat-label>Export Reports</mat-label>
<mat-select [value]="policy()?.exportPolicy">
<mat-option value="FullMask">Full Mask</mat-option>
<mat-option value="PartialReveal">Partial Reveal</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Logs & Telemetry</mat-label>
<mat-select [value]="policy()?.logPolicy" disabled>
<mat-option value="FullMask">Full Mask (Enforced)</mat-option>
</mat-select>
<mat-hint>Secrets are never logged in full</mat-hint>
</mat-form-field>
</div>
</mat-card-content>
</mat-card>
`
})
```
### SDU-005: Findings List Component
```typescript
// secret-findings-list.component.ts
@Component({
selector: 'app-secret-findings-list',
template: `
<div class="findings-container">
<header class="findings-header">
<h2>Secret Findings</h2>
<div class="filters">
<mat-form-field>
<mat-label>Severity</mat-label>
<mat-select multiple [(value)]="severityFilter">
<mat-option value="Critical">Critical</mat-option>
<mat-option value="High">High</mat-option>
<mat-option value="Medium">Medium</mat-option>
<mat-option value="Low">Low</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Status</mat-label>
<mat-select [(value)]="statusFilter">
<mat-option value="Open">Open</mat-option>
<mat-option value="Dismissed">Dismissed</mat-option>
<mat-option value="Excepted">Excepted</mat-option>
</mat-select>
</mat-form-field>
</div>
</header>
<table mat-table [dataSource]="findings()">
<ng-container matColumnDef="severity">
<th mat-header-cell *matHeaderCellDef>Severity</th>
<td mat-cell *matCellDef="let finding">
<app-severity-badge [severity]="finding.severity" />
</td>
</ng-container>
<ng-container matColumnDef="rule">
<th mat-header-cell *matHeaderCellDef>Rule</th>
<td mat-cell *matCellDef="let finding">
<div class="rule-info">
<span class="rule-name">{{ finding.ruleName }}</span>
<span class="rule-category">{{ finding.ruleCategory }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="location">
<th mat-header-cell *matHeaderCellDef>Location</th>
<td mat-cell *matCellDef="let finding">
<code class="file-path">{{ finding.filePath }}:{{ finding.lineNumber }}</code>
</td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef>Detected Value</th>
<td mat-cell *matCellDef="let finding">
<app-masked-secret-value
[value]="finding.value"
[policy]="revelationPolicy()"
[canReveal]="canRevealSecrets()" />
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let finding">
<button mat-icon-button [matMenuTriggerFor]="actionMenu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #actionMenu>
<button mat-menu-item (click)="viewDetails(finding)">
<mat-icon>visibility</mat-icon>
View Details
</button>
<button mat-menu-item (click)="dismiss(finding)">
<mat-icon>cancel</mat-icon>
Dismiss
</button>
<button mat-menu-item (click)="addException(finding)">
<mat-icon>playlist_add</mat-icon>
Add Exception
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"
(click)="viewDetails(row)"></tr>
</table>
</div>
`
})
```
### SDU-006: Masked Value Display
```typescript
// masked-secret-value.component.ts
@Component({
selector: 'app-masked-secret-value',
template: `
<div class="masked-value" [class.revealed]="isRevealed()">
<code>{{ displayValue() }}</code>
@if (canReveal() && !isRevealed()) {
<button
mat-icon-button
matTooltip="Reveal value (logged)"
(click)="reveal()">
<mat-icon>visibility</mat-icon>
</button>
}
@if (isRevealed()) {
<button
mat-icon-button
matTooltip="Hide value"
(click)="hide()">
<mat-icon>visibility_off</mat-icon>
</button>
<button
mat-icon-button
matTooltip="Copy to clipboard"
(click)="copy()">
<mat-icon>content_copy</mat-icon>
</button>
}
</div>
`,
styles: [`
.masked-value {
font-family: monospace;
display: flex;
align-items: center;
gap: 8px;
}
.revealed code {
background: #fff3cd;
padding: 4px 8px;
border-radius: 4px;
}
`]
})
export class MaskedSecretValueComponent {
value = input.required<string>();
policy = input.required<SecretRevelationPolicy>();
canReveal = input<boolean>(false);
private revealed = signal(false);
isRevealed = computed(() => this.revealed() && this.canReveal());
displayValue = computed(() => {
if (this.isRevealed()) {
return this.value();
}
return this.maskValue(this.value(), this.policy());
});
reveal() {
// Log reveal action for audit
this.auditService.logSecretReveal(this.value());
this.revealed.set(true);
}
hide() {
this.revealed.set(false);
}
private maskValue(value: string, policy: SecretRevelationPolicy): string {
switch (policy) {
case 'FullMask':
return '[REDACTED]';
case 'PartialReveal':
if (value.length <= 8) return '*'.repeat(value.length);
return `${value.slice(0, 4)}${'*'.repeat(Math.min(8, value.length - 8))}${value.slice(-4)}`;
default:
return '[REDACTED]';
}
}
}
```
### SDU-010: Alert Destination Configuration
```typescript
// alert-destination-config.component.ts
@Component({
selector: 'app-alert-destination-config',
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Alert Destinations</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="alert-settings">
<mat-slide-toggle [(ngModel)]="settings().enabled">
Enable Alerts
</mat-slide-toggle>
<mat-form-field>
<mat-label>Minimum Severity</mat-label>
<mat-select [(value)]="settings().minimumAlertSeverity">
<mat-option value="Critical">Critical only</mat-option>
<mat-option value="High">High and above</mat-option>
<mat-option value="Medium">Medium and above</mat-option>
<mat-option value="Low">All findings</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-divider />
<h4>Configured Channels</h4>
<div class="destinations-list">
@for (dest of settings().destinations; track dest.id) {
<mat-card class="destination-card">
<div class="destination-info">
<mat-icon>{{ getChannelIcon(dest.channelType) }}</mat-icon>
<span>{{ dest.channelType }}</span>
<code>{{ dest.channelId }}</code>
</div>
<div class="destination-actions">
<button mat-icon-button (click)="testChannel(dest)">
<mat-icon>send</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="removeDestination(dest)">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-card>
}
</div>
<button mat-stroked-button (click)="addDestination()">
<mat-icon>add</mat-icon>
Add Destination
</button>
</mat-card-content>
</mat-card>
`
})
```
## UI Mockups
### Settings Page Layout
```
┌─────────────────────────────────────────────────────────────┐
│ Secret Detection [Enabled ●] │
├─────────────────────────────────────────────────────────────┤
│ [General] [Exceptions] [Alerts] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─ Revelation Policy ─────────────────────────────────┐ │
│ │ ○ Full Mask [REDACTED] │ │
│ │ ● Partial Reveal AKIA****WXYZ │ │
│ │ ○ Full Reveal (requires security-admin) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Rule Categories ───────────────────────────────────┐ │
│ │ ☑ AWS Credentials ☑ GCP Service Accounts │ │
│ │ ☑ Generic API Keys ☑ Private Keys │ │
│ │ ☐ Internal Tokens ☑ Database Credentials │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Findings List
```
┌─────────────────────────────────────────────────────────────┐
│ Secret Findings │
│ Severity: [All ▼] Status: [Open ▼] Image: [All ▼] │
├─────────────────────────────────────────────────────────────┤
│ SEV │ RULE │ LOCATION │ VALUE │
├─────────────────────────────────────────────────────────────┤
│ 🔴 │ AWS Access Key │ config.yaml:42 │ AKIA****XYZ │
│ 🟠 │ Generic API Key │ .env:15 │ sk_l****abc │
│ 🟡 │ Private Key │ certs/server.key │ [REDACTED] │
└─────────────────────────────────────────────────────────────┘
```
## Directory Structure
```
src/Web/StellaOps.Web/src/app/
├── features/
│ └── secret-detection/
│ ├── secret-detection.module.ts
│ ├── secret-detection.routes.ts
│ ├── pages/
│ │ ├── settings/
│ │ │ └── secret-detection-settings.component.ts
│ │ └── findings/
│ │ └── secret-findings-list.component.ts
│ ├── components/
│ │ ├── revelation-policy-config/
│ │ ├── rule-category-selector/
│ │ ├── exception-manager/
│ │ ├── alert-destination-config/
│ │ ├── masked-secret-value/
│ │ └── finding-detail-drawer/
│ └── services/
│ ├── secret-detection-settings.service.ts
│ └── secret-findings.service.ts
```
## Decisions & Risks
| Decision | Rationale |
|----------|-----------|
| Angular Material | Consistent with existing UI |
| Signal-based state | Modern Angular patterns |
| Audit logging on reveal | Compliance requirement |
| Lazy-loaded module | Performance optimization |
## Execution Log
| Date | Action | Notes |
|------|--------|-------|
| 2026-01-04 | Sprint created | UI components for secret detection |
| 2026-01-05 | SDU-001 to SDU-010 completed | Feature module, settings page, revelation policy, rule toggles, findings list, masked display, exception manager, alert config all implemented |
| 2026-01-05 | SDU-011 completed | Channel test functionality added to alert config |
| 2026-01-05 | SDU-012 completed | E2E tests created in e2e/secret-detection.e2e.spec.ts |
| 2026-01-05 | Sprint COMPLETE | All 12 tasks done |