finish secrets finding work and audit remarks work save
This commit is contained in:
@@ -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.** |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user