save progress
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
# Secret Handling & Leak Detection
|
||||
|
||||
## StellaOps approach (2025.11 release)
|
||||
> **Implementation Status:** Secret leak detection (`StellaOps.Scanner.Analyzers.Secrets`) is **NOT YET IMPLEMENTED**.
|
||||
> Only Surface.Secrets (operational credential management) is currently available.
|
||||
> See implementation sprints: SPRINT_20260104_002 through SPRINT_20260104_005.
|
||||
|
||||
## StellaOps approach (TARGET - 2026.01+ release)
|
||||
- Read the Policy/Security briefing: `../../modules/policy/secret-leak-detection-readiness.md`.
|
||||
- Operational runbook: `../../modules/scanner/operations/secret-leak-detection.md`.
|
||||
- Surface.Secrets continues to deliver operational credentials through secure handles (`docs/modules/scanner/design/surface-secrets.md`), with providers supporting Kubernetes Secrets, file bundles, and inline definitions validated by Surface.Validation.
|
||||
@@ -25,15 +29,15 @@
|
||||
- Operators must rely on external tooling for leak detection while Grype focuses exclusively on vulnerability matching.[g1]
|
||||
|
||||
## Key differences
|
||||
- **Purpose**: StellaOps now covers both operational secret retrieval *and* deterministic leak detection; Trivy and Snyk focus exclusively on leak detection while Grype omits it.
|
||||
- **Workflow**: StellaOps performs leak detection in-line during scans with offline rule bundles and policy-aware outcomes; Trivy/Snyk rely on mutable rule packs or SaaS classifiers; Grype delegates to external tools.
|
||||
- **Determinism**: StellaOps signs every bundle and records bundle IDs in explain traces; Trivy and Snyk update rules continuously (risking drift); Grype remains deterministic by not scanning.
|
||||
- **Purpose**: StellaOps currently provides operational secret retrieval only; leak detection is **PLANNED** (see implementation sprints). Trivy and Snyk focus exclusively on leak detection while Grype omits it.
|
||||
- **Workflow**: StellaOps will perform leak detection in-line during scans with offline rule bundles and policy-aware outcomes (when implemented); Trivy/Snyk rely on mutable rule packs or SaaS classifiers; Grype delegates to external tools.
|
||||
- **Determinism**: StellaOps will sign every bundle and record bundle IDs in explain traces (when implemented); Trivy and Snyk update rules continuously (risking drift); Grype remains deterministic by not scanning.
|
||||
|
||||
### Detection technique comparison
|
||||
| Tool | Detection technique(s) | Merge / result handling | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| **StellaOps (≤ 2025.10)** | `Surface.Secrets` providers fetch credentials at runtime; no leak scanning. | Secrets resolve to opaque handles stored in scan metadata; no SBOM entries emitted. | Deterministic secret retrieval only (legacy behaviour). |
|
||||
| **StellaOps (2025.11+)** | `StellaOps.Scanner.Analyzers.Secrets` plug-in executes DSSE-signed rule bundles. | Findings inserted into `ScanAnalysisStore` as `secret.leak` evidence; Policy Engine merges with component context and lattice scores; CLI/export mask payloads. | Rule bundles ship offline, signatures verified locally; see operations runbook for rollout. |
|
||||
| **StellaOps (≤ 2025.10)** | `Surface.Secrets` providers fetch credentials at runtime; no leak scanning. | Secrets resolve to opaque handles stored in scan metadata; no SBOM entries emitted. | Deterministic secret retrieval only. **This is the current implementation.** |
|
||||
| **StellaOps (PLANNED)** | `StellaOps.Scanner.Analyzers.Secrets` plug-in executes DSSE-signed rule bundles. | Findings inserted into `ScanAnalysisStore` as `secret.leak` evidence; Policy Engine merges with component context and lattice scores; CLI/export mask payloads. | **NOT YET IMPLEMENTED** - See SPRINT_20260104_002 through SPRINT_20260104_005. |
|
||||
| **Trivy** | Regex + entropy detectors under `pkg/fanal/secret` (configurable via `trivy-secret.yaml`). | Detectors aggregate per file; results exported alongside vulnerability findings without provenance binding. | Ships built-in rule sets; users can add allow/block lists. |
|
||||
| **Snyk** | Snyk Code SaaS classifiers invoked by CLI plugin (`src/lib/plugins/sast`). | Source uploaded to SaaS; issues returned with severity + remediation; no offline merge with SBOM data. | Requires authenticated cloud access; rules evolve server-side. |
|
||||
| **Grype** | None (focuses on vulnerability matching). | — | Operators must integrate separate tooling for leak detection. |
|
||||
|
||||
1641
docs/full-features-list.md
Normal file
1641
docs/full-features-list.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
# 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 | TODO | None | Scanner Guild | Create project structure and csproj |
|
||||
| 2 | SLD-002 | TODO | None | Scanner Guild | Define SecretRule and SecretRuleset models |
|
||||
| 3 | SLD-003 | TODO | None | Scanner Guild | Implement ISecretDetector interface and RegexDetector |
|
||||
| 4 | SLD-004 | TODO | None | Scanner Guild | Implement EntropyDetector for high-entropy string detection |
|
||||
| 5 | SLD-005 | TODO | None | Scanner Guild | Implement PayloadMasker with configurable masking strategies |
|
||||
| 6 | SLD-006 | TODO | None | Scanner Guild | Define SecretLeakEvidence record and finding model |
|
||||
| 7 | SLD-007 | TODO | SLD-002 | Scanner Guild | Implement RulesetLoader with JSON parsing |
|
||||
| 8 | SLD-008 | TODO | None | Scanner Guild | Add SecretsAnalyzerOptions with feature flag support |
|
||||
| 9 | SLD-009 | TODO | SLD-003,SLD-004 | Scanner Guild | Implement CompositeSecretDetector combining regex and entropy |
|
||||
| 10 | SLD-010 | TODO | SLD-006,SLD-009 | Scanner Guild | Implement SecretsAnalyzer (ILanguageAnalyzer) |
|
||||
| 11 | SLD-011 | TODO | SLD-010 | Scanner Guild | Add SecretsAnalyzerHost for plugin lifecycle |
|
||||
| 12 | SLD-012 | TODO | SLD-011 | Scanner Guild | Integrate with Scanner Worker pipeline |
|
||||
| 13 | SLD-013 | TODO | SLD-010 | Scanner Guild | Add DI registration in ServiceCollectionExtensions |
|
||||
| 14 | SLD-014 | TODO | All | Scanner Guild | Add comprehensive unit tests |
|
||||
| 15 | SLD-015 | TODO | SLD-014 | Scanner Guild | Add integration tests with test fixtures |
|
||||
| 16 | SLD-016 | TODO | 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 |
|
||||
|
||||
451
docs/implplan/SPRINT_20260104_003_SCANNER_secret_rule_bundles.md
Normal file
451
docs/implplan/SPRINT_20260104_003_SCANNER_secret_rule_bundles.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# Sprint 20260104_003_SCANNER - Secret Detection Rule Bundle Infrastructure
|
||||
|
||||
## Topic & Scope
|
||||
|
||||
Implement the DSSE-signed rule bundle infrastructure for secret leak detection. This sprint delivers the signing, verification, and distribution pipeline for deterministic rule bundles.
|
||||
|
||||
**Key deliverables:**
|
||||
1. **Bundle Schema**: Formal JSON schema for rule bundles
|
||||
2. **Bundle Builder**: CLI tool to create and sign rule bundles
|
||||
3. **DSSE Integration**: Signing and verification using existing Signer/Attestor modules
|
||||
4. **Bundle Verification**: Runtime verification of bundle integrity and provenance
|
||||
5. **Default Bundle**: Initial set of secret detection rules
|
||||
|
||||
**Working directory:** `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/`, `src/Cli/`, `offline/rules/secrets/`
|
||||
|
||||
## Dependencies & Concurrency
|
||||
|
||||
- **Depends on**: Sprint 20260104_002 (Core Analyzer), StellaOps.Attestor, StellaOps.Signer
|
||||
- **Required by**: Sprint 20260104_005 (Offline Kit Integration)
|
||||
- **Parallel work**: Tasks RB-001 through RB-005 can be developed concurrently
|
||||
|
||||
## Documentation Prerequisites
|
||||
|
||||
- docs/modules/scanner/operations/secret-leak-detection.md
|
||||
- docs/modules/attestor/architecture.md
|
||||
- docs/modules/signer/architecture.md
|
||||
- docs/ci/dsse-build-flow.md
|
||||
- CLAUDE.md Section 8.6 (DSSE PAE Consistency)
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | RB-001 | DONE | None | Scanner Guild | Define bundle manifest JSON schema |
|
||||
| 2 | RB-002 | DONE | None | Scanner Guild | Define rules JSONL schema and validation |
|
||||
| 3 | RB-003 | DONE | RB-001,RB-002 | Scanner Guild | Create BundleBuilder class for bundle creation |
|
||||
| 4 | RB-004 | DONE | RB-003 | Scanner Guild | Add DSSE signing integration |
|
||||
| 5 | RB-005 | DONE | RB-004 | Scanner Guild | Implement BundleVerifier with Attestor integration |
|
||||
| 6 | RB-006 | DONE | RB-005 | CLI Guild | Add `stella secrets bundle create` CLI command |
|
||||
| 7 | RB-007 | DONE | RB-005 | CLI Guild | Add `stella secrets bundle verify` CLI command |
|
||||
| 8 | RB-008 | DONE | RB-005 | Scanner Guild | Integrate verification into SecretsAnalyzerHost |
|
||||
| 9 | RB-009 | DONE | RB-002 | Scanner Guild | Create default rule definitions |
|
||||
| 10 | RB-010 | DONE | RB-009 | Scanner Guild | Build and sign initial bundle (2026.01) |
|
||||
| 11 | RB-011 | DONE | All | Scanner Guild | Add unit and integration tests |
|
||||
| 12 | RB-012 | DONE | RB-010 | Docs Guild | Document bundle lifecycle and rotation |
|
||||
|
||||
## Task Details
|
||||
|
||||
### RB-001: Bundle Manifest Schema
|
||||
|
||||
Define the manifest schema (`secrets.ruleset.manifest.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://stellaops.io/schemas/secrets-ruleset-manifest-v1.json",
|
||||
"schemaVersion": "1.0",
|
||||
"id": "secrets.ruleset",
|
||||
"version": "2026.01",
|
||||
"createdAt": "2026-01-04T00:00:00Z",
|
||||
"description": "StellaOps Secret Detection Rules",
|
||||
"rules": [
|
||||
{
|
||||
"id": "stellaops.secrets.aws-access-key",
|
||||
"version": "1.0.0",
|
||||
"severity": "high",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"integrity": {
|
||||
"rulesFile": "secrets.ruleset.rules.jsonl",
|
||||
"rulesSha256": "abc123...",
|
||||
"totalRules": 15,
|
||||
"enabledRules": 15
|
||||
},
|
||||
"signatures": {
|
||||
"dsseEnvelope": "secrets.ruleset.dsse.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Bundles/Schemas/`
|
||||
|
||||
### RB-002: Rules JSONL Schema
|
||||
|
||||
Define the rule entry schema for NDJSON format:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "stellaops.secrets.aws-access-key",
|
||||
"version": "1.0.0",
|
||||
"name": "AWS Access Key ID",
|
||||
"description": "Detects AWS Access Key IDs in source code and configuration files",
|
||||
"type": "regex",
|
||||
"pattern": "(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}",
|
||||
"severity": "high",
|
||||
"confidence": "high",
|
||||
"keywords": ["AKIA", "ASIA", "AIDA", "aws"],
|
||||
"filePatterns": ["*.yml", "*.yaml", "*.json", "*.env", "*.properties", "*.conf", "*.config"],
|
||||
"maskingHint": "prefix:4,suffix:2",
|
||||
"metadata": {
|
||||
"category": "cloud-credentials",
|
||||
"provider": "aws",
|
||||
"references": ["https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Validation rules:
|
||||
- `id` must be namespaced (e.g., `stellaops.secrets.*`)
|
||||
- `version` must be valid SemVer
|
||||
- `pattern` must be valid regex (compile-time validation)
|
||||
- `severity` must be one of: low, medium, high, critical
|
||||
- `confidence` must be one of: low, medium, high
|
||||
|
||||
### RB-003: Bundle Builder
|
||||
|
||||
Implement the bundle creation logic:
|
||||
|
||||
```csharp
|
||||
public interface IBundleBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a bundle from individual rule files.
|
||||
/// </summary>
|
||||
Task<BundleArtifact> BuildAsync(
|
||||
BundleBuildOptions options,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record BundleBuildOptions
|
||||
{
|
||||
public required string OutputDirectory { get; init; }
|
||||
public required string BundleId { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required IReadOnlyList<string> RuleFiles { get; init; }
|
||||
public TimeProvider? TimeProvider { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BundleArtifact
|
||||
{
|
||||
public required string ManifestPath { get; init; }
|
||||
public required string RulesPath { get; init; }
|
||||
public required string RulesSha256 { get; init; }
|
||||
public required int TotalRules { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BundleBuilder : IBundleBuilder
|
||||
{
|
||||
// Implementation notes:
|
||||
// - Validate each rule on load
|
||||
// - Sort rules by ID for deterministic output
|
||||
// - Compute SHA-256 of rules file
|
||||
// - Generate manifest with integrity info
|
||||
// - Use TimeProvider for timestamps (determinism)
|
||||
}
|
||||
```
|
||||
|
||||
Location: `src/Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/Bundles/`
|
||||
|
||||
### RB-004: DSSE Signing Integration
|
||||
|
||||
Integrate with Signer module for bundle signing:
|
||||
|
||||
```csharp
|
||||
public interface IBundleSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// Signs a bundle artifact producing a DSSE envelope.
|
||||
/// </summary>
|
||||
Task<DsseEnvelope> SignAsync(
|
||||
BundleArtifact artifact,
|
||||
SigningOptions options,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record SigningOptions
|
||||
{
|
||||
public required string KeyId { get; init; }
|
||||
public string PayloadType { get; init; } = "application/vnd.stellaops.secrets-ruleset+json";
|
||||
public bool IncludeCertificateChain { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed class BundleSigner : IBundleSigner
|
||||
{
|
||||
private readonly ISigner _signer;
|
||||
|
||||
// Implementation notes:
|
||||
// - Use existing Signer infrastructure
|
||||
// - Payload is the manifest JSON (not rules file)
|
||||
// - Include rules file digest in signed payload
|
||||
// - Support multiple signature algorithms
|
||||
}
|
||||
```
|
||||
|
||||
### RB-005: Bundle Verifier
|
||||
|
||||
Implement verification with Attestor integration:
|
||||
|
||||
```csharp
|
||||
public interface IBundleVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies a bundle's DSSE signature and integrity.
|
||||
/// </summary>
|
||||
Task<BundleVerificationResult> VerifyAsync(
|
||||
string bundleDirectory,
|
||||
VerificationOptions options,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed record VerificationOptions
|
||||
{
|
||||
public string? AttestorUrl { get; init; }
|
||||
public bool RequireRekorProof { get; init; } = false;
|
||||
public IReadOnlyList<string>? TrustedKeyIds { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BundleVerificationResult
|
||||
{
|
||||
public required bool IsValid { get; init; }
|
||||
public required string BundleVersion { get; init; }
|
||||
public required DateTimeOffset SignedAt { get; init; }
|
||||
public required string SignerKeyId { get; init; }
|
||||
public string? RekorLogId { get; init; }
|
||||
public IReadOnlyList<string>? ValidationErrors { get; init; }
|
||||
}
|
||||
|
||||
public sealed class BundleVerifier : IBundleVerifier
|
||||
{
|
||||
private readonly IAttestorClient _attestorClient;
|
||||
|
||||
// Implementation notes:
|
||||
// - Verify DSSE envelope signature
|
||||
// - Verify rules file SHA-256 matches manifest
|
||||
// - Optionally verify Rekor transparency log entry
|
||||
// - Support offline verification (no network calls)
|
||||
}
|
||||
```
|
||||
|
||||
### RB-006: CLI Bundle Create Command
|
||||
|
||||
Add CLI command for bundle creation:
|
||||
|
||||
```bash
|
||||
stella secrets bundle create \
|
||||
--output ./bundles/2026.01 \
|
||||
--bundle-id secrets.ruleset \
|
||||
--version 2026.01 \
|
||||
--rules ./rules/*.json \
|
||||
--sign \
|
||||
--key-id stellaops-secrets-signer
|
||||
```
|
||||
|
||||
Implementation in `src/Cli/StellaOps.Cli/Commands/Secrets/`:
|
||||
|
||||
```csharp
|
||||
[Command("secrets bundle create")]
|
||||
public class BundleCreateCommand
|
||||
{
|
||||
[Option("--output", Required = true)]
|
||||
public string OutputDirectory { get; set; }
|
||||
|
||||
[Option("--bundle-id", Required = true)]
|
||||
public string BundleId { get; set; }
|
||||
|
||||
[Option("--version", Required = true)]
|
||||
public string Version { get; set; }
|
||||
|
||||
[Option("--rules", Required = true)]
|
||||
public IReadOnlyList<string> RuleFiles { get; set; }
|
||||
|
||||
[Option("--sign")]
|
||||
public bool Sign { get; set; }
|
||||
|
||||
[Option("--key-id")]
|
||||
public string? KeyId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### RB-007: CLI Bundle Verify Command
|
||||
|
||||
Add CLI command for bundle verification:
|
||||
|
||||
```bash
|
||||
stella secrets bundle verify \
|
||||
--bundle ./bundles/2026.01 \
|
||||
--attestor-url http://attestor.local \
|
||||
--require-rekor
|
||||
```
|
||||
|
||||
```csharp
|
||||
[Command("secrets bundle verify")]
|
||||
public class BundleVerifyCommand
|
||||
{
|
||||
[Option("--bundle", Required = true)]
|
||||
public string BundleDirectory { get; set; }
|
||||
|
||||
[Option("--attestor-url")]
|
||||
public string? AttestorUrl { get; set; }
|
||||
|
||||
[Option("--require-rekor")]
|
||||
public bool RequireRekor { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### RB-008: Analyzer Host Integration
|
||||
|
||||
Update SecretsAnalyzerHost to verify bundles on startup:
|
||||
|
||||
```csharp
|
||||
public sealed class SecretsAnalyzerHost : IHostedService
|
||||
{
|
||||
public async Task StartAsync(CancellationToken ct)
|
||||
{
|
||||
var bundlePath = _options.RulesetPath;
|
||||
|
||||
// Verify bundle integrity
|
||||
var verification = await _verifier.VerifyAsync(bundlePath, new VerificationOptions
|
||||
{
|
||||
RequireRekorProof = _options.RequireSignatureVerification
|
||||
}, ct);
|
||||
|
||||
if (!verification.IsValid)
|
||||
{
|
||||
_logger.LogError("Bundle verification failed: {Errors}",
|
||||
string.Join(", ", verification.ValidationErrors ?? []));
|
||||
|
||||
if (_options.FailOnInvalidBundle)
|
||||
throw new InvalidOperationException("Secret detection bundle verification failed");
|
||||
|
||||
return; // Analyzer disabled
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"SecretsAnalyzerHost: Loaded bundle {Version} signed by {KeyId} with {Count} rules",
|
||||
verification.BundleVersion,
|
||||
verification.SignerKeyId,
|
||||
_ruleset.Rules.Length);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RB-009: Default Rule Definitions
|
||||
|
||||
Create initial rule set in `offline/rules/secrets/sources/`:
|
||||
|
||||
| File | Rule ID | Description |
|
||||
|------|---------|-------------|
|
||||
| `aws-access-key.json` | `stellaops.secrets.aws-access-key` | AWS Access Key ID |
|
||||
| `aws-secret-key.json` | `stellaops.secrets.aws-secret-key` | AWS Secret Access Key |
|
||||
| `github-pat.json` | `stellaops.secrets.github-pat` | GitHub Personal Access Token |
|
||||
| `github-app.json` | `stellaops.secrets.github-app` | GitHub App Token (ghs_, ghp_) |
|
||||
| `gitlab-pat.json` | `stellaops.secrets.gitlab-pat` | GitLab Personal Access Token |
|
||||
| `azure-storage.json` | `stellaops.secrets.azure-storage-key` | Azure Storage Account Key |
|
||||
| `gcp-service-account.json` | `stellaops.secrets.gcp-service-account` | GCP Service Account JSON |
|
||||
| `private-key-rsa.json` | `stellaops.secrets.private-key-rsa` | RSA Private Key (PEM) |
|
||||
| `private-key-ec.json` | `stellaops.secrets.private-key-ec` | EC Private Key (PEM) |
|
||||
| `private-key-openssh.json` | `stellaops.secrets.private-key-openssh` | OpenSSH Private Key |
|
||||
| `jwt.json` | `stellaops.secrets.jwt` | JSON Web Token |
|
||||
| `slack-token.json` | `stellaops.secrets.slack-token` | Slack API Token |
|
||||
| `stripe-key.json` | `stellaops.secrets.stripe-key` | Stripe API Key |
|
||||
| `sendgrid-key.json` | `stellaops.secrets.sendgrid-key` | SendGrid API Key |
|
||||
| `generic-api-key.json` | `stellaops.secrets.generic-api-key` | Generic high-entropy API key |
|
||||
|
||||
### RB-010: Initial Bundle Build
|
||||
|
||||
Create the signed 2026.01 bundle:
|
||||
|
||||
```
|
||||
offline/rules/secrets/2026.01/
|
||||
├── secrets.ruleset.manifest.json
|
||||
├── secrets.ruleset.rules.jsonl
|
||||
└── secrets.ruleset.dsse.json
|
||||
```
|
||||
|
||||
Build script in `.gitea/scripts/build/build-secrets-bundle.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-2026.01}"
|
||||
OUTPUT_DIR="offline/rules/secrets/${VERSION}"
|
||||
|
||||
stella secrets bundle create \
|
||||
--output "${OUTPUT_DIR}" \
|
||||
--bundle-id secrets.ruleset \
|
||||
--version "${VERSION}" \
|
||||
--rules offline/rules/secrets/sources/*.json \
|
||||
--sign \
|
||||
--key-id stellaops-secrets-signer
|
||||
```
|
||||
|
||||
### RB-011: Tests
|
||||
|
||||
Unit tests:
|
||||
- `BundleBuilderTests.cs` - Bundle creation and validation
|
||||
- `BundleVerifierTests.cs` - Signature verification
|
||||
- `RuleValidatorTests.cs` - Rule schema validation
|
||||
|
||||
Integration tests:
|
||||
- `BundleRoundtripTests.cs` - Create, sign, verify cycle
|
||||
- `CliCommandTests.cs` - CLI command execution
|
||||
|
||||
### RB-012: Documentation
|
||||
|
||||
Update `docs/modules/scanner/operations/secret-leak-detection.md`:
|
||||
- Add bundle creation workflow
|
||||
- Document verification process
|
||||
- Add troubleshooting for signature failures
|
||||
|
||||
Create `docs/modules/scanner/operations/secrets-bundle-rotation.md`:
|
||||
- Bundle versioning strategy
|
||||
- Rotation procedures
|
||||
- Rollback instructions
|
||||
|
||||
## Bundle Directory Structure
|
||||
|
||||
```
|
||||
offline/rules/secrets/
|
||||
├── sources/ # Source rule definitions (not distributed)
|
||||
│ ├── aws-access-key.json
|
||||
│ ├── aws-secret-key.json
|
||||
│ └── ...
|
||||
├── 2026.01/ # Signed release bundle
|
||||
│ ├── secrets.ruleset.manifest.json
|
||||
│ ├── secrets.ruleset.rules.jsonl
|
||||
│ └── secrets.ruleset.dsse.json
|
||||
└── latest -> 2026.01 # Symlink to latest stable
|
||||
```
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| NDJSON for rules | Streaming parse, git-friendly, easy validation |
|
||||
| Sign manifest (not rules) | Manifest includes rules digest; smaller signature payload |
|
||||
| Optional Rekor verification | Supports air-gapped deployments |
|
||||
| Symlink for latest | Simple upgrade path, atomic switch |
|
||||
| Source rules in repo | Version control, review process for rule changes |
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Action | Notes |
|
||||
|------|--------|-------|
|
||||
| 2026-01-04 | Sprint created | Part of secret leak detection implementation |
|
||||
| 2026-01-04 | RB-001 to RB-010 | Implemented bundle infrastructure, signing, verification, CLI commands |
|
||||
| 2026-01-04 | RB-011 | Fixed and validated 37 unit tests for bundle system |
|
||||
| 2026-01-04 | RB-012 | Updated secret-leak-detection.md, created secrets-bundle-rotation.md |
|
||||
| 2026-01-04 | Sprint completed | All 12 tasks DONE |
|
||||
|
||||
@@ -0,0 +1,543 @@
|
||||
# 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 | TODO | None | Policy Guild | Define ISecretEvidenceProvider interface |
|
||||
| 2 | PSD-002 | TODO | PSD-001 | Policy Guild | Implement SecretEvidenceContext binding |
|
||||
| 3 | PSD-003 | TODO | None | Policy Guild | Add secret.hasFinding() predicate |
|
||||
| 4 | PSD-004 | TODO | None | Policy Guild | Add secret.bundle.version() predicate |
|
||||
| 5 | PSD-005 | TODO | None | Policy Guild | Add secret.match.count() predicate |
|
||||
| 6 | PSD-006 | TODO | None | Policy Guild | Add secret.mask.applied predicate |
|
||||
| 7 | PSD-007 | TODO | None | Policy Guild | Add secret.path.allowlist() predicate |
|
||||
| 8 | PSD-008 | TODO | PSD-003-007 | Policy Guild | Register predicates in PolicyDslRegistry |
|
||||
| 9 | PSD-009 | TODO | PSD-008 | Policy Guild | Update DSL schema validation |
|
||||
| 10 | PSD-010 | TODO | PSD-008 | Policy Guild | Create example policy templates |
|
||||
| 11 | PSD-011 | TODO | All | Policy Guild | Add unit and integration tests |
|
||||
| 12 | PSD-012 | TODO | 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 |
|
||||
|
||||
591
docs/implplan/SPRINT_20260104_005_AIRGAP_secret_offline_kit.md
Normal file
591
docs/implplan/SPRINT_20260104_005_AIRGAP_secret_offline_kit.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# 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 | TODO | None | AirGap Guild | Update Offline Kit manifest schema for rules |
|
||||
| 2 | OKS-002 | TODO | OKS-001 | AirGap Guild | Add secrets bundle to BundleBuilder |
|
||||
| 3 | OKS-003 | TODO | OKS-002 | AirGap Guild | Create bundle verification in Importer |
|
||||
| 4 | OKS-004 | TODO | None | AirGap Guild | Add Attestor mirror support for bundle verification |
|
||||
| 5 | OKS-005 | TODO | OKS-003 | AirGap Guild | Create bundle installation script |
|
||||
| 6 | OKS-006 | TODO | OKS-005 | AirGap Guild | Add bundle rotation/upgrade workflow |
|
||||
| 7 | OKS-007 | TODO | None | CI/CD Guild | Add bundle to release workflow |
|
||||
| 8 | OKS-008 | TODO | All | AirGap Guild | Add integration tests for offline flow |
|
||||
| 9 | OKS-009 | TODO | All | Docs Guild | Update offline kit documentation |
|
||||
| 10 | OKS-010 | TODO | 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 |
|
||||
|
||||
@@ -28,17 +28,17 @@ Implement adaptive noise-gating for vulnerability graphs to reduce alert fatigue
|
||||
|
||||
| # | Task ID | Status | Key dependency / next step | Owners | Task Definition |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | NG-001 | TODO | None | Guild | Add ProofStrength enum to StellaOps.Evidence.Core |
|
||||
| 2 | NG-002 | TODO | NG-001 | Guild | Add ProofStrength field to EvidenceRecord |
|
||||
| 3 | NG-003 | TODO | None | Guild | Create EdgeSemanticKey and deduplication logic in ReachGraph |
|
||||
| 4 | NG-004 | TODO | None | Guild | Add StabilityDampingGate to Policy.Engine.Gates |
|
||||
| 5 | NG-005 | TODO | NG-004 | Guild | Add StabilityDampingOptions with configurable thresholds |
|
||||
| 6 | NG-006 | TODO | None | Guild | Create DeltaSection enum in VexLens |
|
||||
| 7 | NG-007 | TODO | NG-006 | Guild | Extend VexDelta with section categorization |
|
||||
| 8 | NG-008 | TODO | NG-001,NG-003,NG-004,NG-006 | Guild | Create INoiseGate interface and NoiseGateService |
|
||||
| 9 | NG-009 | TODO | NG-008 | Guild | Add DI registration in VexLensServiceCollectionExtensions |
|
||||
| 10 | NG-010 | TODO | All | Guild | Add unit tests for all new components |
|
||||
| 11 | NG-011 | TODO | NG-010 | Guild | Update module AGENTS.md files |
|
||||
| 1 | NG-001 | DONE | None | Guild | Add ProofStrength enum to StellaOps.Evidence.Core |
|
||||
| 2 | NG-002 | DONE | NG-001 | Guild | Add ProofStrength field to EvidenceRecord |
|
||||
| 3 | NG-003 | DONE | None | Guild | Create EdgeSemanticKey and deduplication logic in ReachGraph |
|
||||
| 4 | NG-004 | DONE | None | Guild | Add StabilityDampingGate to Policy.Engine.Gates |
|
||||
| 5 | NG-005 | DONE | NG-004 | Guild | Add StabilityDampingOptions with configurable thresholds |
|
||||
| 6 | NG-006 | DONE | None | Guild | Create DeltaSection enum in VexLens |
|
||||
| 7 | NG-007 | DONE | NG-006 | Guild | Extend VexDelta with section categorization |
|
||||
| 8 | NG-008 | DONE | NG-001,NG-003,NG-004,NG-006 | Guild | Create INoiseGate interface and NoiseGateService |
|
||||
| 9 | NG-009 | DONE | NG-008 | Guild | Add DI registration in VexLensServiceCollectionExtensions |
|
||||
| 10 | NG-010 | DONE | All | Guild | Add unit tests for all new components |
|
||||
| 11 | NG-011 | DONE | NG-010 | Guild | Update module AGENTS.md files |
|
||||
|
||||
## Task Details
|
||||
|
||||
@@ -184,4 +184,12 @@ Update module documentation:
|
||||
| Date | Action | Notes |
|
||||
|------|--------|-------|
|
||||
| 2026-01-04 | Sprint created | Based on product advisory review |
|
||||
| 2026-01-04 | NG-001,NG-002 | Created ProofStrength enum, ProofStrengthExtensions, ProofRecord in StellaOps.Evidence.Models |
|
||||
| 2026-01-04 | NG-003 | Created EdgeSemanticKey, DeduplicatedEdge, EdgeDeduplicator in StellaOps.ReachGraph.Deduplication |
|
||||
| 2026-01-04 | NG-004,NG-005 | Created StabilityDampingGate, StabilityDampingOptions in StellaOps.Policy.Engine.Gates |
|
||||
| 2026-01-04 | NG-006,NG-007 | Created DeltaSection, DeltaEntry, DeltaReport, DeltaReportBuilder in StellaOps.VexLens.Delta |
|
||||
| 2026-01-04 | NG-008,NG-009 | Created INoiseGate, NoiseGateService, NoiseGateOptions; registered DI in VexLensServiceCollectionExtensions |
|
||||
| 2026-01-04 | NG-010 | Added StabilityDampingGateTests, NoiseGateServiceTests, DeltaReportBuilderTests |
|
||||
| 2026-01-04 | NG-011 | Updated VexLens and Policy.Engine AGENTS.md files |
|
||||
| 2026-01-04 | Sprint complete | All 11 tasks DONE |
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# Sprint 20260104_002_FE - Noise-Gating Delta Report UI
|
||||
|
||||
## Topic & Scope
|
||||
|
||||
Implement frontend components to display noise-gating delta reports from the VexLens backend. This sprint composes existing Angular components to minimize new code while providing a complete UI for:
|
||||
|
||||
1. **Delta Report Display**: Show changes between vulnerability graph snapshots
|
||||
2. **Section-Based Navigation**: Tabs for New, Resolved, ConfidenceUp/Down, PolicyImpact, Damped sections
|
||||
3. **Gating Statistics**: Edge deduplication rates and verdict damping metrics
|
||||
4. **Backend API Endpoints**: Expose DeltaReport via VexLens WebService
|
||||
|
||||
**Working directories:**
|
||||
- `src/Web/StellaOps.Web/src/app/` (frontend)
|
||||
- `src/VexLens/StellaOps.VexLens.WebService/` (backend API)
|
||||
|
||||
## Dependencies & Concurrency
|
||||
|
||||
- Builds on completed Sprint 20260104_001_BE (backend NoiseGate implementation)
|
||||
- Reuses existing components: `DeltaSummaryStripComponent`, `TabsComponent`, `GatingExplainerComponent`
|
||||
- Tasks NG-FE-001 through NG-FE-003 (backend + models) must complete before NG-FE-004+
|
||||
|
||||
## Existing Components to Reuse
|
||||
|
||||
| Component | Location | Usage |
|
||||
|-----------|----------|-------|
|
||||
| `DeltaSummaryStripComponent` | `features/compare/components/` | Overview stats display |
|
||||
| `TabsComponent` / `TabPanelDirective` | `shared/components/tabs/` | Section navigation |
|
||||
| `GatingExplainerComponent` | `features/triage/components/gating-explainer/` | Per-entry explanations |
|
||||
| `DeltaComputeService` patterns | `features/compare/services/` | Signal-based state management |
|
||||
| `GatingReason`, `DeltaSummary` | `features/triage/models/gating.model.ts` | Existing delta/gating types |
|
||||
| `VexStatementStatus` | `core/api/vex-hub.models.ts` | VEX status types |
|
||||
| `BadgeComponent`, `StatCardComponent` | `shared/components/` | Statistics display |
|
||||
|
||||
## Documentation Prerequisites
|
||||
|
||||
- CLAUDE.md (Section 8: Code Quality rules)
|
||||
- src/Web/StellaOps.Web/README.md
|
||||
- docs/modules/vexlens/architecture.md
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
| # | Task ID | Status | Key dependency | Task Definition |
|
||||
|---|---------|--------|----------------|-----------------|
|
||||
| 1 | NG-FE-001 | DONE | None | Add delta report API endpoints to VexLens.WebService |
|
||||
| 2 | NG-FE-002 | DONE | None | Create TypeScript models for noise-gating delta (noise-gating.models.ts) |
|
||||
| 3 | NG-FE-003 | DONE | NG-FE-002 | Create NoiseGatingApiClient service |
|
||||
| 4 | NG-FE-004 | DONE | NG-FE-003 | Create NoiseGatingSummaryStripComponent (extends DeltaSummaryStrip) |
|
||||
| 5 | NG-FE-005 | DONE | NG-FE-003 | Create DeltaEntryCardComponent for individual entries |
|
||||
| 6 | NG-FE-006 | DONE | NG-FE-004,005 | Create NoiseGatingDeltaReportComponent (container with tabs) |
|
||||
| 7 | NG-FE-007 | DONE | NG-FE-006 | Create GatingStatisticsCardComponent |
|
||||
| 8 | NG-FE-008 | DONE | NG-FE-006 | Integrate into vuln-explorer/triage workspace |
|
||||
| 9 | NG-FE-009 | DONE | All | Update feature module exports and routing |
|
||||
|
||||
## Task Details
|
||||
|
||||
### NG-FE-001: Backend API Endpoints
|
||||
|
||||
Add endpoints to `VexLensEndpointExtensions.cs`:
|
||||
|
||||
```csharp
|
||||
// Delta computation
|
||||
POST /api/v1/vexlens/deltas/compute
|
||||
Body: { fromSnapshotId, toSnapshotId, options }
|
||||
Returns: DeltaReportResponse
|
||||
|
||||
// Get gated snapshot
|
||||
GET /api/v1/vexlens/snapshots/{snapshotId}/gated
|
||||
Returns: GatedGraphSnapshotResponse
|
||||
|
||||
// Get gating statistics
|
||||
GET /api/v1/vexlens/gating/statistics
|
||||
Query: tenantId, fromDate, toDate
|
||||
Returns: GatingStatisticsResponse
|
||||
```
|
||||
|
||||
### NG-FE-002: TypeScript Models
|
||||
|
||||
Create `src/app/core/api/noise-gating.models.ts`:
|
||||
|
||||
```typescript
|
||||
// Match backend DeltaSection enum
|
||||
export type NoiseGatingDeltaSection =
|
||||
| 'new' | 'resolved' | 'confidence_up' | 'confidence_down'
|
||||
| 'policy_impact' | 'damped' | 'evidence_changed';
|
||||
|
||||
// Match backend DeltaEntry
|
||||
export interface NoiseGatingDeltaEntry {
|
||||
section: NoiseGatingDeltaSection;
|
||||
vulnerabilityId: string;
|
||||
productKey: string;
|
||||
fromStatus?: VexStatementStatus;
|
||||
toStatus?: VexStatementStatus;
|
||||
fromConfidence?: number;
|
||||
toConfidence?: number;
|
||||
justification?: string;
|
||||
rationaleClass?: string;
|
||||
summary?: string;
|
||||
contributingSources?: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Match backend DeltaReport
|
||||
export interface NoiseGatingDeltaReport {
|
||||
reportId: string;
|
||||
fromSnapshotDigest: string;
|
||||
toSnapshotDigest: string;
|
||||
generatedAt: string;
|
||||
entries: NoiseGatingDeltaEntry[];
|
||||
summary: NoiseGatingDeltaSummary;
|
||||
hasActionableChanges: boolean;
|
||||
}
|
||||
|
||||
// Summary counts
|
||||
export interface NoiseGatingDeltaSummary {
|
||||
totalCount: number;
|
||||
newCount: number;
|
||||
resolvedCount: number;
|
||||
confidenceUpCount: number;
|
||||
confidenceDownCount: number;
|
||||
policyImpactCount: number;
|
||||
dampedCount: number;
|
||||
evidenceChangedCount: number;
|
||||
}
|
||||
|
||||
// Gating statistics
|
||||
export interface GatingStatistics {
|
||||
originalEdgeCount: number;
|
||||
deduplicatedEdgeCount: number;
|
||||
edgeReductionPercent: number;
|
||||
totalVerdictCount: number;
|
||||
surfacedVerdictCount: number;
|
||||
dampedVerdictCount: number;
|
||||
duration: string;
|
||||
}
|
||||
```
|
||||
|
||||
### NG-FE-003: API Client
|
||||
|
||||
Create `src/app/core/api/noise-gating.client.ts`:
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class NoiseGatingApiClient {
|
||||
// Follow VexHubApiHttpClient patterns
|
||||
// Signal-based state management
|
||||
// Caching with Map<string, Observable>
|
||||
}
|
||||
```
|
||||
|
||||
### NG-FE-004: Summary Strip Component
|
||||
|
||||
Extend `DeltaSummaryStripComponent` pattern for noise-gating sections:
|
||||
- New (green), Resolved (blue), ConfidenceUp (teal), ConfidenceDown (orange)
|
||||
- PolicyImpact (red), Damped (gray), EvidenceChanged (purple)
|
||||
|
||||
### NG-FE-005: Delta Entry Card
|
||||
|
||||
Create `delta-entry-card.component.ts`:
|
||||
- Display CVE ID, package, status transition
|
||||
- Confidence change visualization (before -> after with delta %)
|
||||
- Section-specific styling
|
||||
- Link to GatingExplainerComponent for details
|
||||
|
||||
### NG-FE-006: Container Component
|
||||
|
||||
Create `noise-gating-delta-report.component.ts`:
|
||||
- Uses `TabsComponent` with section tabs (badge counts)
|
||||
- Uses `NoiseGatingSummaryStripComponent` for overview
|
||||
- Filterable entry list within each tab
|
||||
- Follows three-pane pattern from compare feature
|
||||
|
||||
### NG-FE-007: Statistics Card
|
||||
|
||||
Create `gating-statistics-card.component.ts`:
|
||||
- Edge reduction percentage visualization
|
||||
- Verdict surfacing/damping ratios
|
||||
- Processing duration display
|
||||
- Follows `StatCardComponent` patterns
|
||||
|
||||
### NG-FE-008: Triage Integration
|
||||
|
||||
Add to vuln-explorer:
|
||||
- "Delta Report" tab or drawer
|
||||
- Trigger from snapshot comparison
|
||||
- Link from finding detail to delta context
|
||||
|
||||
### NG-FE-009: Module Exports
|
||||
|
||||
Update feature module:
|
||||
- Export new components
|
||||
- Add to routing if needed
|
||||
- Register API client
|
||||
|
||||
## Decisions & Risks
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Compose existing components | ~70% code reuse, consistent UX |
|
||||
| Signal-based state | Matches existing Angular 17 patterns |
|
||||
| Section tabs vs flat list | Better UX for categorized changes |
|
||||
| Lazy-load delta data | Large reports should not block initial render |
|
||||
|
||||
## Execution Log
|
||||
|
||||
| Date | Action | Notes |
|
||||
|------|--------|-------|
|
||||
| 2026-01-04 | Sprint created | Based on backend noise-gating completion |
|
||||
| 2026-01-04 | NG-FE-001 | Added endpoints to VexLensEndpointExtensions.cs, created NoiseGatingApiModels.cs |
|
||||
| 2026-01-04 | NG-FE-001 | Created ISnapshotStore, IGatingStatisticsStore with in-memory implementations |
|
||||
| 2026-01-04 | NG-FE-001 | Updated INoiseGate.DiffAsync to accept DeltaReportOptions |
|
||||
| 2026-01-04 | NG-FE-001 | Registered storage services in VexLensServiceCollectionExtensions |
|
||||
| 2026-01-04 | NG-FE-002 | Created noise-gating.models.ts with all API types and helper functions |
|
||||
| 2026-01-04 | NG-FE-003 | Created noise-gating.client.ts with signal-based state and caching |
|
||||
| 2026-01-04 | NG-FE-004 | Created NoiseGatingSummaryStripComponent with section badges |
|
||||
| 2026-01-04 | NG-FE-005 | Created DeltaEntryCardComponent for individual entries |
|
||||
| 2026-01-04 | NG-FE-006 | Created NoiseGatingDeltaReportComponent container with tabs |
|
||||
| 2026-01-04 | NG-FE-007 | Created GatingStatisticsCardComponent with progress bars |
|
||||
| 2026-01-04 | NG-FE-009 | Created index.ts barrel export for noise-gating components |
|
||||
| 2026-01-04 | NG-FE-008 | Integrated noise-gating into TriageCanvasComponent with Delta tab |
|
||||
| 2026-01-04 | NG-FE-008 | Added keyboard shortcut 'd' for delta tab |
|
||||
| 2026-01-04 | NG-FE-008 | Updated triage components index.ts to export noise-gating components |
|
||||
| 2026-01-04 | Sprint complete | All 9 tasks completed |
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> **Core Thesis:** Stella Ops isn't a scanner that outputs findings. It's a platform that outputs **attestable decisions that can be replayed**. That difference survives auditors, regulators, and supply-chain propagation.
|
||||
|
||||
> **Looking for the complete feature catalog?** See [`full-features-list.md`](full-features-list.md) for the comprehensive list of all platform capabilities, or [`04_FEATURE_MATRIX.md`](04_FEATURE_MATRIX.md) for tier-by-tier availability.
|
||||
|
||||
---
|
||||
|
||||
## At a Glance
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
# Secret Leak Detection (Scanner Operations)
|
||||
|
||||
> **Status:** Preview (Sprint 132). Requires `SCANNER-ENG-0007`/`POLICY-READINESS-0001` release bundle and the experimental flag `secret-leak-detection`.
|
||||
> **Status:** PLANNED - Implementation in progress. See implementation sprints below.
|
||||
>
|
||||
> **Previous status:** Preview (Sprint 132). Requires `SCANNER-ENG-0007`/`POLICY-READINESS-0001` release bundle and the experimental flag `secret-leak-detection`.
|
||||
>
|
||||
> **Audience:** Scanner operators, Security Guild, Docs Guild, Offline Kit maintainers.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Component | Status | Sprint |
|
||||
|-----------|--------|--------|
|
||||
| `StellaOps.Scanner.Analyzers.Secrets` plugin | NOT IMPLEMENTED | [SPRINT_20260104_002](../../../implplan/SPRINT_20260104_002_SCANNER_secret_leak_detection_core.md) |
|
||||
| Rule bundle infrastructure | NOT IMPLEMENTED | [SPRINT_20260104_003](../../../implplan/SPRINT_20260104_003_SCANNER_secret_rule_bundles.md) |
|
||||
| Policy DSL predicates (`secret.*`) | NOT IMPLEMENTED | [SPRINT_20260104_004](../../../implplan/SPRINT_20260104_004_POLICY_secret_dsl_integration.md) |
|
||||
| Offline Kit integration | NOT IMPLEMENTED | [SPRINT_20260104_005](../../../implplan/SPRINT_20260104_005_AIRGAP_secret_offline_kit.md) |
|
||||
| Surface.Secrets (credential delivery) | IMPLEMENTED | N/A (already complete) |
|
||||
|
||||
**Note:** The remainder of this document describes the TARGET SPECIFICATION for secret leak detection. The feature is not yet available. Surface.Secrets (operational credential management) is fully implemented and separate from secret leak detection.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope & goals
|
||||
|
||||
- Introduce the **`StellaOps.Scanner.Analyzers.Secrets`** plug-in, which executes deterministic rule bundles against layer content during scans.
|
||||
@@ -29,29 +45,107 @@ Rule bundles ship in the Export Center / Offline Kit under `offline/rules/secret
|
||||
| --- | --- | --- |
|
||||
| `secrets.ruleset.manifest.json` | Lists rule IDs, versions, severity defaults, and hash digests. | Consume during policy drift audits. |
|
||||
| `secrets.ruleset.rules.jsonl` | Newline-delimited definitions (regex/entropy metadata, masking hints). | Loaded by the analyzer at startup. |
|
||||
| `secrets.ruleset.dsse.json` | DSSE envelope (Signer certificate chain + Attestor proof). | Verify before distributing bundles. |
|
||||
| `secrets.ruleset.dsse.json` | DSSE envelope (HMAC-SHA256 signature). | Verify before distributing bundles. |
|
||||
|
||||
Verification checklist (`stella excititor verify` talks to the configured Attestor service):
|
||||
### 3.1 Creating custom bundles
|
||||
|
||||
Organizations can create custom rule bundles with additional detection patterns:
|
||||
|
||||
```bash
|
||||
# Create a bundle from rule definition files
|
||||
stella secrets bundle create \
|
||||
--output ./bundles/custom-2026.01 \
|
||||
--bundle-id custom.secrets.ruleset \
|
||||
--version 2026.01 \
|
||||
--rules ./custom-rules/*.json
|
||||
|
||||
# Create and sign in one step
|
||||
stella secrets bundle create \
|
||||
--output ./bundles/custom-2026.01 \
|
||||
--bundle-id custom.secrets.ruleset \
|
||||
--version 2026.01 \
|
||||
--rules ./custom-rules/*.json \
|
||||
--sign \
|
||||
--key-id my-org-secrets-signer \
|
||||
--shared-secret-file /path/to/signing.key
|
||||
```
|
||||
|
||||
Rule definition files must follow the JSON schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myorg.secrets.internal-api-key",
|
||||
"version": "1.0.0",
|
||||
"name": "Internal API Key",
|
||||
"description": "Detects internal API keys with MYORG prefix",
|
||||
"type": "regex",
|
||||
"pattern": "MYORG_[A-Z0-9]{32}",
|
||||
"severity": "high",
|
||||
"confidence": "high",
|
||||
"keywords": ["MYORG_"],
|
||||
"filePatterns": ["*.env", "*.yaml", "*.json"],
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**Rule ID requirements:**
|
||||
- Must be namespaced (e.g., `myorg.secrets.rule-name`)
|
||||
- Must start with lowercase letter
|
||||
- May contain lowercase letters, digits, dots, and hyphens
|
||||
|
||||
### 3.2 Verifying bundles
|
||||
|
||||
Verify bundle integrity and signature before deployment:
|
||||
|
||||
```bash
|
||||
# Basic verification (checks SHA-256 integrity)
|
||||
stella secrets bundle verify \
|
||||
--bundle ./bundles/2026.01
|
||||
|
||||
# Full verification with signature check
|
||||
stella secrets bundle verify \
|
||||
--bundle ./bundles/2026.01 \
|
||||
--shared-secret-file /path/to/signing.key
|
||||
|
||||
# Verify with trusted key list
|
||||
stella secrets bundle verify \
|
||||
--bundle ./bundles/2026.01 \
|
||||
--shared-secret-file /path/to/signing.key \
|
||||
--trusted-key-ids stellaops-secrets-signer,my-org-signer
|
||||
```
|
||||
|
||||
For air-gapped environments, use the local verification mode (no network calls):
|
||||
|
||||
```bash
|
||||
stella secrets bundle verify \
|
||||
--bundle ./bundles/2026.01 \
|
||||
--shared-secret-file /path/to/signing.key \
|
||||
--skip-rekor
|
||||
```
|
||||
|
||||
Alternatively, use `stella excititor verify` for Attestor-based verification:
|
||||
|
||||
```bash
|
||||
stella excititor verify \
|
||||
--attestation offline/rules/secrets/2025.11/secrets.ruleset.dsse.json \
|
||||
--digest $(sha256sum offline/rules/secrets/2025.11/secrets.ruleset.rules.jsonl | cut -d' ' -f1)
|
||||
--attestation offline/rules/secrets/2026.01/secrets.ruleset.dsse.json \
|
||||
--digest $(sha256sum offline/rules/secrets/2026.01/secrets.ruleset.rules.jsonl | cut -d' ' -f1)
|
||||
```
|
||||
|
||||
For air-gapped environments point the CLI at the Offline Kit Attestor mirror (for example `STELLA_ATTESTOR_URL=http://attestor.offline.local`) before running the command. The Attestor instance validates the DSSE envelope against the mirrored Rekor log and embedded certificate chain; no public network access is required.
|
||||
### 3.3 Deploying bundles
|
||||
|
||||
Once verified, copy the manifest + rules to the worker:
|
||||
Once verified, copy the bundle to the worker:
|
||||
|
||||
```
|
||||
/opt/stellaops/plugins/scanner/analyzers/secrets/
|
||||
├── secrets.ruleset.manifest.json
|
||||
├── secrets.ruleset.rules.jsonl
|
||||
└── secrets.ruleset.dsse.json
|
||||
|- secrets.ruleset.manifest.json
|
||||
|- secrets.ruleset.rules.jsonl
|
||||
|- secrets.ruleset.dsse.json
|
||||
```
|
||||
|
||||
Restart the worker so the analyzer reloads the updated bundle. Bundles are immutable; upgrading requires replacing all three files and restarting.
|
||||
|
||||
See [secrets-bundle-rotation.md](./secrets-bundle-rotation.md) for rotation procedures and rollback instructions.
|
||||
|
||||
## 4. Enabling the analyzer
|
||||
|
||||
1. **Toggle the feature flag** (WebService + Worker):
|
||||
@@ -161,6 +255,52 @@ rule low_confidence_warn priority 20 {
|
||||
| No findings despite seeded secrets | Ensure bundle hash matches manifest. Run worker with `--secrets-trace` (debug build) to log matched rules locally. |
|
||||
| Policy marks findings as unknown | Upgrade tenant policies to include `secret.*` helpers; older policies silently drop the namespace. |
|
||||
| Air-gapped verification fails | Ensure `STELLA_ATTESTOR_URL` points to the Offline Kit Attestor mirror and rerun `stella excititor verify --attestation <file> --digest <sha256>`. |
|
||||
| Signature verification failed | Check shared secret matches signing key. Verify DSSE envelope exists and is not corrupted. See signature troubleshooting below. |
|
||||
| Bundle integrity check failed | Rules file was modified after signing. Re-download bundle or rebuild from sources. |
|
||||
| Key not in trusted list | Add signer key ID to `--trusted-key-ids` or update `scanner.secrets.trustedKeyIds` configuration. |
|
||||
|
||||
### 7.1 Signature verification troubleshooting
|
||||
|
||||
**"Signature verification failed" error:**
|
||||
|
||||
1. Verify the shared secret is correct:
|
||||
```bash
|
||||
# Check secret file exists and is readable
|
||||
cat /path/to/signing.key | wc -c
|
||||
# Should output the key length (typically 32-64 bytes for base64-encoded keys)
|
||||
```
|
||||
|
||||
2. Check the DSSE envelope format:
|
||||
```bash
|
||||
# Verify envelope is valid JSON
|
||||
jq . ./bundles/2026.01/secrets.ruleset.dsse.json
|
||||
```
|
||||
|
||||
3. Confirm manifest matches envelope payload:
|
||||
```bash
|
||||
# The envelope payload (base64url-decoded) should match the manifest content
|
||||
# excluding the signatures field
|
||||
```
|
||||
|
||||
**"Rules file integrity check failed" error:**
|
||||
|
||||
1. Recompute the SHA-256 hash:
|
||||
```bash
|
||||
sha256sum ./bundles/2026.01/secrets.ruleset.rules.jsonl
|
||||
```
|
||||
|
||||
2. Compare with manifest:
|
||||
```bash
|
||||
jq -r '.integrity.rulesSha256' ./bundles/2026.01/secrets.ruleset.manifest.json
|
||||
```
|
||||
|
||||
3. If hashes differ, the rules file was modified. Re-download or rebuild the bundle.
|
||||
|
||||
**"Bundle is not signed" error:**
|
||||
|
||||
The bundle was created without the `--sign` flag. Either:
|
||||
- Rebuild with signing: `stella secrets bundle create ... --sign --key-id <key>`
|
||||
- Skip signature verification: `--skip-signature-verification` (not recommended for production)
|
||||
|
||||
## 8. References
|
||||
|
||||
|
||||
298
docs/modules/scanner/operations/secrets-bundle-rotation.md
Normal file
298
docs/modules/scanner/operations/secrets-bundle-rotation.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Secret Detection Bundle Rotation
|
||||
|
||||
> **Audience:** Scanner operators, Security Guild, Offline Kit maintainers.
|
||||
>
|
||||
> **Related:** [secret-leak-detection.md](./secret-leak-detection.md)
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Secret detection rule bundles are versioned, immutable artifacts that define the patterns used to detect leaked credentials. This document covers the versioning strategy, rotation procedures, and rollback instructions.
|
||||
|
||||
## 2. Versioning strategy
|
||||
|
||||
Bundles follow CalVer (Calendar Versioning) with the format `YYYY.MM`:
|
||||
|
||||
| Version | Release Type | Notes |
|
||||
|---------|--------------|-------|
|
||||
| `2026.01` | Monthly release | Standard monthly update |
|
||||
| `2026.01.1` | Patch release | Critical rule fix within the month |
|
||||
| `2026.02` | Monthly release | Next scheduled release |
|
||||
|
||||
**Version precedence:**
|
||||
- `2026.02` > `2026.01.1` > `2026.01`
|
||||
- Patch versions (`YYYY.MM.N`) are only used for critical fixes
|
||||
- Monthly releases reset the patch counter
|
||||
|
||||
**Custom bundles:**
|
||||
Organizations creating custom bundles should use a prefix to avoid conflicts:
|
||||
- `myorg.2026.01` for organization-specific bundles
|
||||
- Or semantic versioning: `1.0.0`, `1.1.0`, etc.
|
||||
|
||||
## 3. Release cadence
|
||||
|
||||
| Release Type | Frequency | Notification |
|
||||
|--------------|-----------|--------------|
|
||||
| Monthly release | First week of each month | Release notes, changelog |
|
||||
| Patch release | As needed for critical rules | Security advisory |
|
||||
| Breaking changes | Major version bump | Migration guide |
|
||||
|
||||
## 4. Rotation procedures
|
||||
|
||||
### 4.1 Downloading the new bundle
|
||||
|
||||
```bash
|
||||
# From the Export Center or Offline Kit
|
||||
curl -O https://export.stellaops.io/rules/secrets/2026.02/secrets.ruleset.manifest.json
|
||||
curl -O https://export.stellaops.io/rules/secrets/2026.02/secrets.ruleset.rules.jsonl
|
||||
curl -O https://export.stellaops.io/rules/secrets/2026.02/secrets.ruleset.dsse.json
|
||||
```
|
||||
|
||||
For air-gapped environments, obtain the bundle from the Offline Kit media.
|
||||
|
||||
### 4.2 Verifying the bundle
|
||||
|
||||
Always verify bundles before deployment:
|
||||
|
||||
```bash
|
||||
stella secrets bundle verify \
|
||||
--bundle ./2026.02 \
|
||||
--shared-secret-file /etc/stellaops/secrets-signing.key \
|
||||
--trusted-key-ids stellaops-secrets-signer
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Bundle verified successfully.
|
||||
Bundle ID: secrets.ruleset
|
||||
Version: 2026.02
|
||||
Rule count: 18
|
||||
Enabled rules: 18
|
||||
Signed by: stellaops-secrets-signer
|
||||
Signed at: 2026-02-01T00:00:00Z
|
||||
```
|
||||
|
||||
### 4.3 Staged rollout
|
||||
|
||||
For production environments, use a staged rollout:
|
||||
|
||||
**Stage 1: Canary (1 worker)**
|
||||
```bash
|
||||
# Deploy to canary worker
|
||||
scp -r ./2026.02/* canary-worker:/opt/stellaops/plugins/scanner/analyzers/secrets/
|
||||
ssh canary-worker 'systemctl restart stellaops-scanner-worker'
|
||||
|
||||
# Monitor for 24 hours
|
||||
# Check logs, metrics, and finding counts
|
||||
```
|
||||
|
||||
**Stage 2: Ring 1 (10% of workers)**
|
||||
```bash
|
||||
# Deploy to ring 1 workers
|
||||
ansible-playbook -l ring1 deploy-secrets-bundle.yml -e bundle_version=2026.02
|
||||
```
|
||||
|
||||
**Stage 3: Full rollout (all workers)**
|
||||
```bash
|
||||
# Deploy to all workers
|
||||
ansible-playbook deploy-secrets-bundle.yml -e bundle_version=2026.02
|
||||
```
|
||||
|
||||
### 4.4 Atomic deployment
|
||||
|
||||
For single-worker deployments or when downtime is acceptable:
|
||||
|
||||
```bash
|
||||
# Stop the worker
|
||||
systemctl stop stellaops-scanner-worker
|
||||
|
||||
# Backup current bundle
|
||||
cp -r /opt/stellaops/plugins/scanner/analyzers/secrets{,.backup}
|
||||
|
||||
# Deploy new bundle
|
||||
cp -r ./2026.02/* /opt/stellaops/plugins/scanner/analyzers/secrets/
|
||||
|
||||
# Start the worker
|
||||
systemctl start stellaops-scanner-worker
|
||||
|
||||
# Verify startup
|
||||
journalctl -u stellaops-scanner-worker | grep SecretsAnalyzerHost
|
||||
```
|
||||
|
||||
### 4.5 Using symlinks (recommended)
|
||||
|
||||
For zero-downtime rotations, use the symlink pattern:
|
||||
|
||||
```bash
|
||||
# Directory structure
|
||||
/opt/stellaops/plugins/scanner/analyzers/secrets/
|
||||
bundles/
|
||||
2026.01/
|
||||
secrets.ruleset.manifest.json
|
||||
secrets.ruleset.rules.jsonl
|
||||
secrets.ruleset.dsse.json
|
||||
2026.02/
|
||||
secrets.ruleset.manifest.json
|
||||
secrets.ruleset.rules.jsonl
|
||||
secrets.ruleset.dsse.json
|
||||
current -> bundles/2026.02 # Symlink
|
||||
```
|
||||
|
||||
Rotation with symlinks:
|
||||
```bash
|
||||
# Deploy new bundle (no restart needed yet)
|
||||
cp -r ./2026.02 /opt/stellaops/plugins/scanner/analyzers/secrets/bundles/
|
||||
|
||||
# Atomic switch
|
||||
ln -sfn bundles/2026.02 /opt/stellaops/plugins/scanner/analyzers/secrets/current
|
||||
|
||||
# Restart worker to pick up new bundle
|
||||
systemctl restart stellaops-scanner-worker
|
||||
```
|
||||
|
||||
## 5. Rollback procedures
|
||||
|
||||
### 5.1 Quick rollback
|
||||
|
||||
If issues are detected after deployment:
|
||||
|
||||
```bash
|
||||
# With symlinks (fastest)
|
||||
ln -sfn bundles/2026.01 /opt/stellaops/plugins/scanner/analyzers/secrets/current
|
||||
systemctl restart stellaops-scanner-worker
|
||||
|
||||
# Without symlinks
|
||||
cp -r /opt/stellaops/plugins/scanner/analyzers/secrets.backup/* \
|
||||
/opt/stellaops/plugins/scanner/analyzers/secrets/
|
||||
systemctl restart stellaops-scanner-worker
|
||||
```
|
||||
|
||||
### 5.2 Identifying rollback triggers
|
||||
|
||||
Roll back immediately if you observe:
|
||||
|
||||
| Symptom | Likely Cause | Action |
|
||||
|---------|--------------|--------|
|
||||
| Worker fails to start | Bundle corruption or invalid rules | Rollback + investigate |
|
||||
| Finding count drops to zero | All rules disabled or regex errors | Rollback + check manifest |
|
||||
| Finding count spikes 10x+ | Overly broad new patterns | Rollback + review rules |
|
||||
| High CPU usage | Catastrophic regex backtracking | Rollback + report to Security Guild |
|
||||
| Signature verification failures | Key mismatch or tampering | Rollback + verify bundle source |
|
||||
|
||||
### 5.3 Post-rollback verification
|
||||
|
||||
After rolling back:
|
||||
|
||||
```bash
|
||||
# Verify worker is healthy
|
||||
systemctl status stellaops-scanner-worker
|
||||
|
||||
# Check bundle version in logs
|
||||
journalctl -u stellaops-scanner-worker | grep "Loaded bundle"
|
||||
|
||||
# Verify finding generation (run a test scan)
|
||||
stella scan --target test-image:latest --secrets-only
|
||||
```
|
||||
|
||||
## 6. Bundle retention
|
||||
|
||||
Retain previous bundle versions for rollback capability:
|
||||
|
||||
| Environment | Retention |
|
||||
|-------------|-----------|
|
||||
| Production | Last 3 versions |
|
||||
| Staging | Last 2 versions |
|
||||
| Development | Latest only |
|
||||
|
||||
Cleanup script:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BUNDLE_DIR=/opt/stellaops/plugins/scanner/analyzers/secrets/bundles
|
||||
KEEP=3
|
||||
|
||||
ls -dt ${BUNDLE_DIR}/*/ | tail -n +$((KEEP+1)) | xargs rm -rf
|
||||
```
|
||||
|
||||
## 7. Monitoring rotation
|
||||
|
||||
Key metrics to monitor during rotation:
|
||||
|
||||
| Metric | Baseline | Alert Threshold |
|
||||
|--------|----------|-----------------|
|
||||
| `scanner.secret.finding_total` | Varies | +/- 50% from baseline |
|
||||
| `scanner.secret.scan_duration_ms` | < 100ms | > 500ms |
|
||||
| `scanner.secret.bundle_load_errors` | 0 | > 0 |
|
||||
| Worker restart success | 100% | < 100% |
|
||||
|
||||
Prometheus alert example:
|
||||
```yaml
|
||||
- alert: SecretBundleRotationAnomaly
|
||||
expr: |
|
||||
abs(
|
||||
sum(rate(scanner_secret_finding_total[5m]))
|
||||
- sum(rate(scanner_secret_finding_total[5m] offset 1h))
|
||||
) / sum(rate(scanner_secret_finding_total[5m] offset 1h)) > 0.5
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Secret finding rate changed significantly after bundle rotation"
|
||||
```
|
||||
|
||||
## 8. Air-gapped rotation
|
||||
|
||||
For air-gapped environments:
|
||||
|
||||
1. **Obtain bundle from secure media:**
|
||||
```bash
|
||||
# Mount offline kit media
|
||||
mount /dev/sr0 /mnt/offline-kit
|
||||
|
||||
# Copy bundle
|
||||
cp -r /mnt/offline-kit/rules/secrets/2026.02 \
|
||||
/opt/stellaops/plugins/scanner/analyzers/secrets/bundles/
|
||||
```
|
||||
|
||||
2. **Verify with local secret:**
|
||||
```bash
|
||||
stella secrets bundle verify \
|
||||
--bundle /opt/stellaops/plugins/scanner/analyzers/secrets/bundles/2026.02 \
|
||||
--shared-secret-file /etc/stellaops/offline-signing.key \
|
||||
--skip-rekor
|
||||
```
|
||||
|
||||
3. **Follow standard rotation procedure (Section 4).**
|
||||
|
||||
## 9. Emergency procedures
|
||||
|
||||
### 9.1 Disabling secret detection
|
||||
|
||||
If secret detection must be disabled entirely:
|
||||
|
||||
```bash
|
||||
# Disable via configuration
|
||||
echo 'scanner.features.experimental.secret-leak-detection: false' >> /etc/stellaops/scanner.yaml
|
||||
|
||||
# Restart worker
|
||||
systemctl restart stellaops-scanner-worker
|
||||
```
|
||||
|
||||
### 9.2 Emergency rule disable
|
||||
|
||||
To disable a specific problematic rule without full rotation:
|
||||
|
||||
1. Edit the manifest to set `enabled: false` for the rule
|
||||
2. This breaks signature verification (expected)
|
||||
3. Configure worker to skip signature verification temporarily:
|
||||
```yaml
|
||||
scanner:
|
||||
secrets:
|
||||
skipSignatureVerification: true # TEMPORARY - re-enable after fix
|
||||
```
|
||||
4. Restart worker
|
||||
5. Request emergency patch release from Security Guild
|
||||
|
||||
## 10. References
|
||||
|
||||
- [secret-leak-detection.md](./secret-leak-detection.md) - Main secret detection documentation
|
||||
- [SPRINT_20260104_003_SCANNER_secret_rule_bundles.md](../../../implplan/SPRINT_20260104_003_SCANNER_secret_rule_bundles.md) - Implementation sprint
|
||||
- [dsse-rekor-operator-guide.md](./dsse-rekor-operator-guide.md) - DSSE and Rekor verification
|
||||
Reference in New Issue
Block a user