From f7d27c6fda63b8ba9b193af9b13256bb2ea2e9f5 Mon Sep 17 00:00:00 2001 From: StellaOps Bot Date: Sun, 4 Jan 2026 15:44:49 +0200 Subject: [PATCH] feat(secrets): Implement secret leak policies and signal binding - Added `spl-secret-block@1.json` to block deployments with critical or high severity secret findings. - Introduced `spl-secret-warn@1.json` to warn on secret findings without blocking deployments. - Created `SecretSignalBinder.cs` to bind secret evidence to policy evaluation signals. - Developed unit tests for `SecretEvidenceContext` and `SecretSignalBinder` to ensure correct functionality. - Enhanced `SecretSignalContextExtensions` to integrate secret evidence into signal contexts. --- docs/full-features-list.md | 1662 +++++++---------- ...1_BE_determinism_timeprovider_injection.md | 2 +- ...60104_004_POLICY_secret_dsl_integration.md | 30 +- .../policy/secret-leak-detection-readiness.md | 83 +- .../SecretSignalContextExtensions.cs | 106 ++ .../Schemas/signals-schema@1.json | 159 ++ .../Schemas/spl-schema@1.json | 21 +- .../Schemas/spl-secret-block@1.json | 82 + .../Schemas/spl-secret-warn@1.json | 98 + .../Secrets/SecretSignalBinder.cs | 228 +++ .../StellaOps.Policy/StellaOps.Policy.csproj | 3 + .../Secrets/SecretEvidenceContextTests.cs | 259 +++ .../Secrets/SecretSignalBinderTests.cs | 264 +++ .../SecretSignalContextExtensionsTests.cs | 182 ++ .../StellaOps.PolicyDsl.Tests.csproj | 3 +- .../InMemoryWebhookRateLimiter.cs | 8 +- .../GraphJobs/GraphJobService.cs | 14 +- .../ISystemClock.cs | 17 +- .../PolicyRuns/InMemoryPolicyRunService.cs | 22 +- .../StellaOps.Scheduler.WebService/Program.cs | 2 +- .../SchedulerEndpointHelpers.cs | 6 +- .../Schedules/InMemorySchedulerServices.cs | 15 +- .../StellaOps.Scheduler.WebService.csproj | 1 + .../FailureSignatureRepository.cs | 16 +- .../Repositories/ImpactSnapshotRepository.cs | 7 +- .../Postgres/Repositories/JobRepository.cs | 16 +- .../Repositories/TriggerRepository.cs | 11 +- .../StellaOps.Scheduler.Persistence.csproj | 1 + .../Attestor/BundleRotationJob.cs | 13 +- .../Console/EvidenceBundleCoordinator.cs | 22 +- .../Console/ProgressStreamingWorker.cs | 6 +- .../Events/SchedulerEventPublisher.cs | 9 +- .../Exception/ExpiringNotificationWorker.cs | 13 +- .../Execution/HttpScannerReportClient.cs | 9 +- .../Execution/PartitionHealthMonitor.cs | 6 +- .../Planning/ScoreReplaySchedulerJob.cs | 9 +- .../Policy/GateEvaluationJob.cs | 21 +- .../Reachability/ReachabilityJoinerWorker.cs | 14 +- .../ReachabilityStalenessMonitor.cs | 8 +- .../Resolver/EvaluationOrchestrationWorker.cs | 16 +- .../Simulation/SimulationReducerWorker.cs | 8 +- .../Simulation/SimulationSecurityEnforcer.cs | 9 +- .../StellaOps.Scheduler.Worker.csproj | 1 + .../GraphJobServiceTests.cs | 31 +- 44 files changed, 2406 insertions(+), 1107 deletions(-) create mode 100644 src/Policy/StellaOps.PolicyDsl/SecretSignalContextExtensions.cs create mode 100644 src/Policy/__Libraries/StellaOps.Policy/Schemas/signals-schema@1.json create mode 100644 src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-block@1.json create mode 100644 src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-warn@1.json create mode 100644 src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretSignalBinder.cs create mode 100644 src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretEvidenceContextTests.cs create mode 100644 src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretSignalBinderTests.cs create mode 100644 src/Policy/__Tests/StellaOps.PolicyDsl.Tests/SecretSignalContextExtensionsTests.cs diff --git a/docs/full-features-list.md b/docs/full-features-list.md index d70230b14..8e5cfe886 100644 --- a/docs/full-features-list.md +++ b/docs/full-features-list.md @@ -1,1073 +1,777 @@ -# Full Features List - Stella Ops +# Stella Ops - Complete Features Catalog -> **Comprehensive catalog of every capability in the Stella Ops platform.** +> **Comprehensive table of every capability in the platform.** > -> For quick capability cards with competitive differentiation, see [`key-features.md`](key-features.md). -> For tier-based availability (Free/Community/Enterprise), see [`04_FEATURE_MATRIX.md`](04_FEATURE_MATRIX.md). +> For competitive differentiation highlights, see [`key-features.md`](key-features.md). +> For tier-based pricing details, see [`04_FEATURE_MATRIX.md`](04_FEATURE_MATRIX.md). --- -## How to Read This Document +## Legend -- **Base Features**: Core functionality available to all users -- **Enhanced Features**: Advanced capabilities building on base features -- **Specialized Features**: Domain-specific or enterprise-grade capabilities -- **Control Method**: Indicates how each feature is accessed - - `CLI` - Command-line interface - - `Config` - YAML/JSON configuration files - - `UI` - Web user interface - - `API` - REST/gRPC API endpoints +| Symbol | Meaning | +|--------|---------| +| Y | Available | +| - | Not available | +| Limited | Partial functionality | +| Coming | Planned feature | + +**Tiers:** Free (F), Community (C), Enterprise (E) --- ## Table of Contents -### Part I: Foundational Capabilities -1. [Container Scanning](#1-container-scanning) -2. [Package Detection](#2-package-detection) -3. [Vulnerability Detection](#3-vulnerability-detection) -4. [Output & Reporting](#4-output--reporting) - -### Part II: Enhanced Analysis -5. [SBOM Management](#5-sbom-management) -6. [VEX Processing](#6-vex-processing) -7. [Reachability Analysis](#7-reachability-analysis) -8. [Policy Engine](#8-policy-engine) - -### Part III: Specialized Capabilities -9. [Determinism & Reproducibility](#9-determinism--reproducibility) -10. [Attestation & Signing](#10-attestation--signing) -11. [Offline Operations](#11-offline-operations) -12. [Risk Scoring](#12-risk-scoring) - -### Part IV: Platform Features -13. [Authentication & Authorization](#13-authentication--authorization) -14. [Deployment & Operations](#14-deployment--operations) -15. [Integrations](#15-integrations) -16. [Observability](#16-observability) - -### Appendices -- [A. CLI Command Reference](#appendix-a-cli-command-reference) -- [B. Configuration Reference](#appendix-b-configuration-reference) -- [C. API Reference](#appendix-c-api-reference) +1. [Container & Image Scanning](#1-container--image-scanning) +2. [Package Detection - Operating Systems](#2-package-detection---operating-systems) +3. [Package Detection - Language Ecosystems](#3-package-detection---language-ecosystems) +4. [Vulnerability Data Sources](#4-vulnerability-data-sources) +5. [Vulnerability Enrichment](#5-vulnerability-enrichment) +6. [SBOM Capabilities](#6-sbom-capabilities) +7. [Output Formats](#7-output-formats) +8. [Filtering & Thresholds](#8-filtering--thresholds) +9. [VEX Processing](#9-vex-processing) +10. [Reachability Analysis](#10-reachability-analysis) +11. [Secrets Detection](#11-secrets-detection) +12. [Policy Engine](#12-policy-engine) +13. [Policy Gates](#13-policy-gates) +14. [Risk Scoring](#14-risk-scoring) +15. [Comparison & Diff](#15-comparison--diff) +16. [Deterministic Replay](#16-deterministic-replay) +17. [Attestation & Signing](#17-attestation--signing) +18. [Cryptography Profiles](#18-cryptography-profiles) +19. [Offline & Air-Gap](#19-offline--air-gap) +20. [Verification](#20-verification) +21. [Authentication](#21-authentication) +22. [Authorization & Access Control](#22-authorization--access-control) +23. [Evidence Management](#23-evidence-management) +24. [Observability](#24-observability) +25. [Notifications](#25-notifications) +26. [CI/CD Integration](#26-cicd-integration) +27. [Registry Integration](#27-registry-integration) +28. [Deployment Options](#28-deployment-options) +29. [Storage & Infrastructure](#29-storage--infrastructure) +30. [Web UI Features](#30-web-ui-features) --- -# Part I: Foundational Capabilities +## 1. Container & Image Scanning -## 1. Container Scanning - -Container scanning is the core capability of Stella Ops. All other features build upon this foundation. - -### 1.1 Image Scanning (Base) - -Scan container images for vulnerabilities and generate SBOMs. - -| Feature | Description | Control | -|---------|-------------|---------| -| OCI image scanning | Scan OCI-compliant container images | `CLI` `API` | -| Docker image scanning | Scan Docker images from local daemon or registry | `CLI` `API` | -| Filesystem scanning | Scan extracted rootfs directories | `CLI` | -| Archive scanning | Scan .tar.gz container archives | `CLI` | -| Digest-based pull | Pull images by content-addressable digest | `CLI` | - -**CLI Usage:** -```bash -stella scan --image -stella scan --image -stella scan --rootfs /path/to/extracted -``` - -### 1.2 Registry Integration (Base) - -Connect to container registries for scanning. - -| Feature | Description | Control | -|---------|-------------|---------| -| Public registry | Scan images from public registries (Docker Hub, GHCR, etc.) | `CLI` | -| Private registry | Authenticate to private registries | `CLI` `Config` | -| Registry auth | Username/password, token, and keychain authentication | `Config` | -| Mirror support | Use registry mirrors for offline environments | `Config` | - -**Configuration:** -```yaml -# etc/scanner.yaml -registry: - mirrors: - docker.io: "mirror.internal:5000" - credentials: - - registry: "private.registry.io" - username: "${REGISTRY_USER}" - password: "${REGISTRY_PASSWORD}" -``` - -### 1.3 Layer Analysis (Enhanced) - -Analyze container layers for package changes. - -| Feature | Description | Control | -|---------|-------------|---------| -| Per-layer detection | Identify which packages came from which layer | `CLI` | -| Base image detection | Automatically identify base image | `CLI` | -| Layer change tracking | Track package additions/removals per layer | `CLI` | -| Inherited vs added | Distinguish base image packages from application packages | `CLI` | - -**CLI Usage:** -```bash -stella scan --image myapp:latest --show-layers -``` - -### 1.4 Scan Performance (Enhanced) - -Performance optimizations for scanning at scale. - -| Feature | Description | Control | -|---------|-------------|---------| -| Delta-SBOM cache | Cache layer SBOMs for sub-second warm scans | `Config` | -| Concurrent workers | Run multiple scan workers in parallel | `Config` | -| Content-addressed caching | Deduplicate layers by content hash | Automatic | -| Incremental analysis | Only analyze changed layers | Automatic | - -**Performance Tiers:** -- **Free**: 1 concurrent scan worker -- **Community**: 3 concurrent scan workers -- **Enterprise**: Unlimited concurrent workers +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Scan image by tag | Scan container image using registry tag | `stella scan --image registry/app:tag` | Y | Y | Y | +| Scan image by digest | Scan container image using content-addressable digest | `stella scan --image registry/app@sha256:...` | Y | Y | Y | +| Scan local Docker image | Scan image from local Docker daemon | `stella scan --image myapp:local` | Y | Y | Y | +| Scan filesystem | Scan extracted container rootfs directory | `stella scan --rootfs /path/to/rootfs` | Y | Y | Y | +| Scan tar archive | Scan container image from .tar.gz archive | `stella scan --archive image.tar.gz` | Y | Y | Y | +| Layer-by-layer analysis | Analyze each container layer separately | Automatic during scan | Y | Y | Y | +| Base image detection | Identify the base image used | Automatic during scan | Y | Y | Y | +| Base image separation | Separate base image vulns from app vulns | `--show-layers` flag | Y | Y | Y | +| Delta-SBOM caching | Cache layer SBOMs for faster warm scans | Configure in `scanner.yaml` | - | Y | Y | +| Sub-second warm scans | Achieve <1s scan times for cached images | Automatic with caching | - | Y | Y | +| Concurrent scan workers | Run multiple scans in parallel | Configure `scanner.workers` | 1 | 3 | Unlimited | +| Scan queue management | Queue and prioritize scan jobs | Configure in `scheduler.yaml` | - | Y | Y | +| Scan timeout control | Set maximum scan duration | `--timeout 300` | Y | Y | Y | +| Scan retry on failure | Automatically retry failed scans | Configure in `scanner.yaml` | - | Y | Y | --- -## 2. Package Detection +## 2. Package Detection - Operating Systems -### 2.1 OS Packages (Base) - -Detect operating system packages and their vulnerabilities. - -| Feature | Description | Control | -|---------|-------------|---------| -| Alpine APK | Detect Alpine Linux packages | Automatic | -| Debian/Ubuntu APT | Detect apt packages from dpkg database | Automatic | -| RHEL/CentOS/Fedora RPM | Detect RPM packages | Automatic | -| Arch Linux Pacman | Detect Arch packages | Automatic | -| SUSE Zypper | Detect SUSE packages | Automatic | - -### 2.2 Language Ecosystems (Base) - -Detect packages from application language ecosystems. - -| Ecosystem | Package Manager | Manifest Files | Control | -|-----------|-----------------|----------------|---------| -| **JavaScript/Node.js** | npm, yarn, pnpm | package.json, package-lock.json, yarn.lock | Automatic | -| **Python** | pip, poetry, pipenv | requirements.txt, Pipfile.lock, pyproject.toml | Automatic | -| **Java** | Maven, Gradle | pom.xml, build.gradle, *.jar | Automatic | -| **Go** | Go Modules | go.mod, go.sum | Automatic | -| **.NET** | NuGet | *.csproj, packages.config, *.deps.json | Automatic | -| **Ruby** | Bundler | Gemfile, Gemfile.lock | Automatic | -| **Rust** | Cargo | Cargo.toml, Cargo.lock | Automatic | -| **PHP** | Composer | composer.json, composer.lock | Automatic | -| **Bun** | Bun | bun.lockb, package.json | Automatic | -| **Deno** | Deno | deno.json, import_map.json | Automatic | -| **Native/C/C++** | conan, vcpkg | conanfile.txt, vcpkg.json | Automatic | - -### 2.3 Advanced Detection (Enhanced) - -Enhanced package detection capabilities. - -| Feature | Description | Control | -|---------|-------------|---------| -| Transitive dependency mapping | Map full dependency tree | Automatic | -| License detection | Detect package licenses | `CLI` | -| Binary fingerprinting | Identify packages from compiled binaries | `CLI` | -| Symbol extraction | Extract symbol tables for reachability | `CLI` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Alpine APK packages | Detect packages from Alpine Linux | Automatic | Y | Y | Y | +| Debian dpkg packages | Detect packages from Debian/Ubuntu | Automatic | Y | Y | Y | +| Ubuntu packages | Detect packages from Ubuntu | Automatic | Y | Y | Y | +| RHEL RPM packages | Detect packages from Red Hat Enterprise Linux | Automatic | Y | Y | Y | +| CentOS RPM packages | Detect packages from CentOS | Automatic | Y | Y | Y | +| Fedora RPM packages | Detect packages from Fedora | Automatic | Y | Y | Y | +| Rocky Linux packages | Detect packages from Rocky Linux | Automatic | Y | Y | Y | +| AlmaLinux packages | Detect packages from AlmaLinux | Automatic | Y | Y | Y | +| Oracle Linux packages | Detect packages from Oracle Linux | Automatic | Y | Y | Y | +| Amazon Linux packages | Detect packages from Amazon Linux | Automatic | Y | Y | Y | +| SUSE zypper packages | Detect packages from SUSE/openSUSE | Automatic | Y | Y | Y | +| Arch Linux pacman | Detect packages from Arch Linux | Automatic | Y | Y | Y | +| Photon OS packages | Detect packages from VMware Photon OS | Automatic | Y | Y | Y | +| CBL-Mariner packages | Detect packages from Microsoft CBL-Mariner | Automatic | Y | Y | Y | +| Wolfi packages | Detect packages from Wolfi | Automatic | Y | Y | Y | +| Chainguard packages | Detect packages from Chainguard images | Automatic | Y | Y | Y | --- -## 3. Vulnerability Detection +## 3. Package Detection - Language Ecosystems -### 3.1 Advisory Sources (Base) - -Vulnerability data sources used for detection. - -| Source | Description | Update Frequency | Control | -|--------|-------------|------------------|---------| -| NVD (NIST) | National Vulnerability Database | Hourly | `Config` | -| GitHub Security Advisories (GHSA) | GitHub ecosystem advisories | Real-time | `Config` | -| OSV | Open Source Vulnerabilities | Real-time | `Config` | -| Alpine SecDB | Alpine-specific advisories | Hourly | `Config` | -| Debian Tracker | Debian-specific advisories | Hourly | `Config` | -| RHEL/CentOS Errata | Red Hat security errata | Daily | `Config` | -| Ubuntu USN | Ubuntu Security Notices | Hourly | `Config` | - -**Configuration:** -```yaml -# etc/concelier.yaml -concelier: - sources: - ghsa: - apiToken: "${GITHUB_PAT}" - pageSize: 50 - nvd: - enabled: true - apiKey: "${NVD_API_KEY}" -``` - -### 3.2 Vulnerability Enrichment (Enhanced) - -Additional metadata added to detected vulnerabilities. - -| Feature | Description | Control | -|---------|-------------|---------| -| KEV (Known Exploited) | CISA Known Exploited Vulnerabilities flag | Automatic | -| EPSS | Exploit Prediction Scoring System percentile | Automatic | -| CVSS v4.0 | CVSS 4.0 scoring with environmental metrics | Automatic | -| Exploit maturity | Proof of concept, weaponized, in-the-wild | Automatic | - -### 3.3 Backport Detection (Specialized) - -Detect security patches backported by vendors. - -| Feature | Description | Control | -|---------|-------------|---------| -| Binary backport detection | Identify backported patches in binaries | `CLI` | -| Changelog evidence | Match changelogs to security fixes | Automatic | -| Vendor VEX integration | Apply vendor-provided VEX statements | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| npm packages | Detect Node.js npm packages | Automatic from package-lock.json | Y | Y | Y | +| yarn packages | Detect Node.js yarn packages | Automatic from yarn.lock | Y | Y | Y | +| pnpm packages | Detect Node.js pnpm packages | Automatic from pnpm-lock.yaml | Y | Y | Y | +| Python pip packages | Detect pip packages | Automatic from requirements.txt | Y | Y | Y | +| Python poetry packages | Detect poetry packages | Automatic from poetry.lock | Y | Y | Y | +| Python pipenv packages | Detect pipenv packages | Automatic from Pipfile.lock | Y | Y | Y | +| Python conda packages | Detect conda packages | Automatic from conda-lock.yml | Y | Y | Y | +| Java Maven dependencies | Detect Maven dependencies | Automatic from pom.xml | Y | Y | Y | +| Java Gradle dependencies | Detect Gradle dependencies | Automatic from build.gradle | Y | Y | Y | +| Java JAR analysis | Analyze embedded JARs for dependencies | Automatic | Y | Y | Y | +| Java WAR/EAR analysis | Analyze web archives for dependencies | Automatic | Y | Y | Y | +| Go modules | Detect Go module dependencies | Automatic from go.mod, go.sum | Y | Y | Y | +| .NET NuGet packages | Detect NuGet packages | Automatic from *.csproj, packages.config | Y | Y | Y | +| .NET deps.json analysis | Analyze .NET deps.json files | Automatic | Y | Y | Y | +| Ruby Bundler gems | Detect Ruby gems | Automatic from Gemfile.lock | Y | Y | Y | +| Rust Cargo crates | Detect Rust crates | Automatic from Cargo.lock | Y | Y | Y | +| PHP Composer packages | Detect Composer packages | Automatic from composer.lock | Y | Y | Y | +| Bun packages | Detect Bun packages | Automatic from bun.lockb | Y | Y | Y | +| Deno imports | Detect Deno imports | Automatic from deno.json, import_map.json | Y | Y | Y | +| Swift packages | Detect Swift Package Manager packages | Automatic from Package.resolved | Y | Y | Y | +| Conan packages | Detect C/C++ Conan packages | Automatic from conanfile.txt | Y | Y | Y | +| vcpkg packages | Detect C/C++ vcpkg packages | Automatic from vcpkg.json | Y | Y | Y | +| Hex packages | Detect Elixir Hex packages | Automatic from mix.lock | Y | Y | Y | +| Pub packages | Detect Dart/Flutter packages | Automatic from pubspec.lock | Y | Y | Y | +| Transitive dependencies | Map complete dependency tree | Automatic | Y | Y | Y | +| Dependency path tracking | Show how each dependency was introduced | In scan output | Y | Y | Y | +| License detection | Identify package licenses | Automatic, show with `--licenses` | Y | Y | Y | +| Binary fingerprinting | Identify packages from compiled binaries | `--binary-analysis` | - | Y | Y | +| Symbol extraction | Extract symbol tables from binaries | Automatic with binary analysis | - | Y | Y | --- -## 4. Output & Reporting +## 4. Vulnerability Data Sources -### 4.1 Output Formats (Base) - -Standard output formats for scan results. - -| Format | Description | Control | -|--------|-------------|---------| -| Table (human-readable) | Formatted table output for terminal | `CLI --output table` | -| JSON | Machine-readable JSON output | `CLI --output json` | -| SARIF | Static Analysis Results Interchange Format | `CLI --output sarif` | -| CycloneDX VEX | CycloneDX VEX format | `CLI --output cdx-vex` | -| OpenVEX | OpenVEX format | `CLI --output openvex` | - -**CLI Usage:** -```bash -stella scan --image myapp:latest --output json > results.json -stella scan --image myapp:latest --output sarif > results.sarif -``` - -### 4.2 Filtering & Thresholds (Base) - -Filter and threshold controls for scan results. - -| Feature | Description | Control | -|---------|-------------|---------| -| Severity filter | Filter by CRITICAL, HIGH, MEDIUM, LOW | `CLI --severity` | -| Fix available | Show only vulnerabilities with fixes | `CLI --fixable` | -| Exit codes | Configurable exit codes for CI/CD | `CLI` | -| Ignore file | .stellaignore for suppression | `Config` | - -**CLI Usage:** -```bash -stella scan --image myapp --severity HIGH,CRITICAL --fixable -stella scan --image myapp --exit-code-if-vuln 1 -``` - -### 4.3 Export Center (Enhanced) - -Batch export and report generation. - -| Feature | Description | Control | -|---------|-------------|---------| -| Scheduled exports | Export scan results on schedule | `Config` `UI` | -| Multiple formats | Export to JSON, CSV, PDF, Excel | `API` | -| Template-based reports | Customizable report templates | `Config` | -| Compliance reports | Pre-built compliance report templates | `UI` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| NVD (NIST) | National Vulnerability Database | Configure in `concelier.yaml` | Y | Y | Y | +| GitHub Security Advisories | GHSA ecosystem advisories | Configure with `GITHUB_PAT` | Y | Y | Y | +| OSV database | Open Source Vulnerabilities | Automatic | Y | Y | Y | +| Alpine SecDB | Alpine Linux security database | Automatic | Y | Y | Y | +| Debian Security Tracker | Debian vulnerability tracker | Automatic | Y | Y | Y | +| Ubuntu USN | Ubuntu Security Notices | Automatic | Y | Y | Y | +| Red Hat OVAL | Red Hat security data | Automatic | Y | Y | Y | +| Red Hat Security Errata | RHEL security errata | Automatic | Y | Y | Y | +| SUSE OVAL | SUSE security data | Automatic | Y | Y | Y | +| Amazon Linux Security | Amazon Linux advisories | Automatic | Y | Y | Y | +| Oracle Linux OVAL | Oracle Linux security data | Automatic | Y | Y | Y | +| Photon Security Advisories | VMware Photon advisories | Automatic | Y | Y | Y | +| Wolfi Security Advisories | Wolfi security data | Automatic | Y | Y | Y | +| CISA KEV | Known Exploited Vulnerabilities catalog | Automatic | Y | Y | Y | +| Custom advisory feeds | Import custom advisory sources | Configure in `concelier.yaml` | - | Y | Y | +| Advisory feed scheduling | Configure update frequency | Configure in `concelier.yaml` | - | Y | Y | +| Advisory feed mirroring | Mirror feeds locally | Configure Mirror service | - | - | Y | --- -# Part II: Enhanced Analysis +## 5. Vulnerability Enrichment -## 5. SBOM Management - -### 5.1 SBOM Generation (Base) - -Generate Software Bill of Materials. - -| Format | Version | Description | Control | -|--------|---------|-------------|---------| -| CycloneDX | 1.7 | Primary output format | `CLI --sbom-format cyclonedx` | -| CycloneDX | 1.6 | Backward compatible | `CLI --sbom-format cyclonedx-1.6` | -| SPDX | 3.0.1 | SPDX 3.0.1 format | `CLI --sbom-format spdx` | -| SPDX-JSON | 2.3 | SPDX JSON format | `CLI --sbom-format spdx-json` | - -**CLI Usage:** -```bash -stella scan --image myapp --sbom-out sbom.json --sbom-format cyclonedx -``` - -### 5.2 SBOM Ingestion (Base) - -Import existing SBOMs. - -| Feature | Description | Control | -|---------|-------------|---------| -| Auto-format detection | Automatically detect SBOM format | Automatic | -| BYOS (Bring Your Own SBOM) | Scan using provided SBOM | `CLI --sbom` | -| Third-party SBOM | Import SBOMs from external sources | `API` | -| Validation | Validate SBOM structure and content | `CLI` | - -**CLI Usage:** -```bash -stella scan --sbom existing-sbom.json -``` - -### 5.3 SBOM Diff (Enhanced) - -Compare SBOMs between versions. - -| Feature | Description | Control | -|---------|-------------|---------| -| Package diff | Show added/removed packages | `CLI` | -| Version diff | Show version changes | `CLI` | -| License diff | Show license changes | `CLI` | -| Semantic diff | Understand meaning of changes | `CLI` | - -**CLI Usage:** -```bash -stella compare sbom --a v1.0-sbom.json --b v2.0-sbom.json -``` - -### 5.4 SBOM Lineage Ledger (Enterprise) - -Full version history and lineage tracking. - -| Feature | Description | Control | -|---------|-------------|---------| -| Version history | Full SBOM version history | `API` `UI` | -| Lineage tracking | Track SBOM across builds | `API` | -| Traversal queries | Query SBOM lineage | `API` | -| Audit trail | Complete audit trail | `UI` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| CVSS v2.0 scores | Include CVSS 2.0 base scores | Automatic | Y | Y | Y | +| CVSS v3.0 scores | Include CVSS 3.0 base scores | Automatic | Y | Y | Y | +| CVSS v3.1 scores | Include CVSS 3.1 base scores | Automatic | Y | Y | Y | +| CVSS v4.0 scores | Include CVSS 4.0 base scores | Automatic | Y | Y | Y | +| CVSS environmental metrics | Apply environmental context | Configure CVSS policy | - | Y | Y | +| CVSS temporal metrics | Apply temporal context | Automatic from feed data | Y | Y | Y | +| KEV flagging | Flag Known Exploited Vulnerabilities | Automatic | Y | Y | Y | +| EPSS scores | Exploit Prediction Scoring System | Automatic | Y | Y | Y | +| EPSS percentile | Show EPSS percentile ranking | Automatic | Y | Y | Y | +| Exploit maturity | Show exploit availability status | Automatic | Y | Y | Y | +| Proof of concept available | Flag when PoC exists | Automatic | Y | Y | Y | +| Weaponized exploit | Flag weaponized exploits | Automatic | Y | Y | Y | +| In-the-wild exploitation | Flag active exploitation | Automatic from KEV + feeds | Y | Y | Y | +| Fix available | Show if fix version exists | Automatic | Y | Y | Y | +| Fix version | Show the version that fixes the vuln | Automatic | Y | Y | Y | +| Vendor advisory links | Link to vendor advisories | Automatic | Y | Y | Y | +| CWE mapping | Map to CWE weakness types | Automatic | Y | Y | Y | +| CAPEC mapping | Map to CAPEC attack patterns | Automatic | - | Y | Y | --- -## 6. VEX Processing +## 6. SBOM Capabilities -### 6.1 VEX Ingestion (Base) - -Import VEX statements from multiple sources. - -| Format | Description | Control | -|--------|-------------|---------| -| OpenVEX | OpenVEX JSON format | `CLI` `API` | -| CycloneDX VEX | CycloneDX VEX format | `CLI` `API` | -| CSAF | Common Security Advisory Framework | `CLI` `API` | - -**CLI Usage:** -```bash -stella vex import --file vendor-vex.json -``` - -### 6.2 VEX Statuses (Base) - -Standard VEX status types. - -| Status | Description | Policy Effect | -|--------|-------------|---------------| -| `not_affected` | Component not affected by vulnerability | Suppresses finding | -| `affected` | Component is affected | Surfaces finding | -| `fixed` | Vulnerability has been fixed | Contextual | -| `under_investigation` | Investigation in progress | Marks as Unknown | - -### 6.3 VEX Consensus Engine (Enhanced) - -K4 lattice logic for VEX consensus. - -| Feature | Description | Control | -|---------|-------------|---------| -| Multi-issuer consensus | Merge VEX from multiple issuers | `Config` | -| Trust weighting | Weight VEX by issuer trust level | `Config` | -| Conflict detection | Detect conflicting VEX statements | Automatic | -| K4 lattice logic | Belnap four-valued logic (Unknown, True, False, Conflict) | Automatic | - -**K4 Lattice Values:** -- `Unknown` - No information available -- `True` - Positive assertion (affected) -- `False` - Negative assertion (not affected) -- `Conflict` - Contradictory assertions - -### 6.4 Issuer Directory (Enhanced) - -Manage trusted VEX issuers. - -| Feature | Description | Control | -|---------|-------------|---------| -| Issuer registry | Register trusted VEX issuers | `Config` `UI` | -| Trust levels | Assign trust weights to issuers | `Config` | -| CSAF publisher discovery | Discover CSAF publishers | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| CycloneDX 1.7 generation | Generate CycloneDX 1.7 SBOMs | `--sbom-out sbom.json --sbom-format cyclonedx` | Y | Y | Y | +| CycloneDX 1.6 generation | Generate CycloneDX 1.6 SBOMs | `--sbom-format cyclonedx-1.6` | Y | Y | Y | +| CycloneDX 1.5 generation | Generate CycloneDX 1.5 SBOMs | `--sbom-format cyclonedx-1.5` | Y | Y | Y | +| SPDX 3.0.1 generation | Generate SPDX 3.0.1 SBOMs | `--sbom-format spdx` | Y | Y | Y | +| SPDX 2.3 generation | Generate SPDX 2.3 SBOMs | `--sbom-format spdx-2.3` | Y | Y | Y | +| SPDX-JSON generation | Generate SPDX JSON format | `--sbom-format spdx-json` | Y | Y | Y | +| SBOM auto-format detection | Detect format of imported SBOMs | Automatic | Y | Y | Y | +| SBOM import (CycloneDX) | Import CycloneDX SBOMs | `stella scan --sbom file.json` | Y | Y | Y | +| SBOM import (SPDX) | Import SPDX SBOMs | `stella scan --sbom file.spdx` | Y | Y | Y | +| SBOM import (Trivy JSON) | Import Trivy JSON format | `stella scan --sbom trivy.json` | Y | Y | Y | +| SBOM validation | Validate SBOM structure | Automatic on import | Y | Y | Y | +| SBOM normalization | Normalize imported SBOMs | Automatic | Y | Y | Y | +| SBOM deduplication | Deduplicate SBOM components | Automatic | Y | Y | Y | +| SBOM storage | Store SBOMs in central repository | Automatic via SbomService | - | Y | Y | +| SBOM versioning | Track SBOM versions over time | Via SbomService API | - | Y | Y | +| SBOM lineage tracking | Track SBOM lineage across builds | Via Lineage API | - | - | Y | +| SBOM traversal queries | Query SBOM history and relationships | Via Lineage API | - | - | Y | +| SBOM retention policies | Configure SBOM retention periods | Configure in `sbom-service.yaml` | - | Y | Y | --- -## 7. Reachability Analysis +## 7. Output Formats -### 7.1 Static Reachability (Enhanced) - -Determine if vulnerable code is reachable. - -| Feature | Description | Control | -|---------|-------------|---------| -| Call graph analysis | Build call graph from entrypoint | `CLI` | -| Reachable/Unreachable classification | Mark vulnerabilities by reachability | `CLI` | -| Path visualization | Show call paths to vulnerable code | `CLI` | - -**CLI Usage:** -```bash -stella scan --image myapp --reachability -stella graph show --cve CVE-2024-1234 --artifact sha256:abc... -``` - -### 7.2 Three-Layer Proofs (Specialized) - -Multi-layer reachability validation. - -| Layer | What It Proves | Confidence | -|-------|---------------|------------| -| Static | Call graph shows path exists | Likely | -| Binary | Compiled binary contains symbol | Higher | -| Runtime | eBPF probe confirms execution | Confirmed | - -**Confidence Tiers:** -- **Confirmed** - All three layers agree -- **Likely** - Static + binary agree; no runtime -- **Present** - Package present; no reachability evidence -- **Unreachable** - Static analysis proves no path - -### 7.3 Signed Reachability (Specialized) - -Cryptographic binding for reachability proofs. - -| Feature | Description | Control | -|---------|-------------|---------| -| Graph-level DSSE | Sign entire reachability graph | `Config` | -| Edge-bundle attestation | Sign individual path edges | `Config` | -| Proof export | Export reachability proofs | `CLI` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Table output | Human-readable table format | `--output table` (default) | Y | Y | Y | +| JSON output | Machine-readable JSON | `--output json` | Y | Y | Y | +| SARIF output | Static Analysis Results Format | `--output sarif` | Y | Y | Y | +| CycloneDX VEX output | CycloneDX VEX format | `--output cdx-vex` | Y | Y | Y | +| OpenVEX output | OpenVEX format | `--output openvex` | Y | Y | Y | +| CSV output | Comma-separated values | `--output csv` | Y | Y | Y | +| Markdown output | Markdown formatted report | `--output markdown` | Y | Y | Y | +| HTML output | HTML formatted report | `--output html` | - | Y | Y | +| PDF output | PDF formatted report | Via Export Center | - | - | Y | +| Excel output | Excel spreadsheet format | Via Export Center | - | - | Y | +| Template-based output | Custom output templates | Configure templates | - | - | Y | +| Output to file | Write output to file | `--output-file results.json` | Y | Y | Y | +| Output to stdout | Write output to stdout | Default behavior | Y | Y | Y | +| Quiet mode | Suppress non-essential output | `--quiet` | Y | Y | Y | +| Verbose mode | Show detailed output | `--verbose` | Y | Y | Y | --- -## 8. Policy Engine +## 8. Filtering & Thresholds -### 8.1 Policy Packs (Base) - -Pre-built and custom policy configurations. - -| Feature | Description | Control | -|---------|-------------|---------| -| Built-in starter pack | Production-ready Day 1 policy | `CLI policy install starter-day1` | -| Custom policies | Define custom policy rules | `Config` | -| Policy validation | Validate policy YAML syntax | `CLI policy validate` | - -**CLI Usage:** -```bash -stella policy install starter-day1 -stella policy validate --path ./my-policy.yaml -stella policy list-packs -``` - -### 8.2 Policy Rules (Base) - -Define rules for vulnerability handling. - -| Rule Type | Description | Example | -|-----------|-------------|---------| -| Severity block | Block by severity level | Block CRITICAL reachable | -| Reachability gate | Gate based on reachability | Allow unreachable HIGH | -| VEX bypass | Allow VEX-suppressed findings | Allow with VEX:not_affected | -| Unknowns budget | Set unknowns threshold | Fail if unknowns > 5% | - -**Policy Example:** -```yaml -apiVersion: policy.stellaops.io/v1 -kind: PolicyPack -metadata: - name: production-gates -spec: - rules: - - name: block-reachable-critical - action: block - severity: [CRITICAL] - reachability: reachable - message: "Reachable critical vulnerabilities must be fixed" - - name: allow-unreachable - action: allow - reachability: unreachable -``` - -### 8.3 Policy Simulation (Enhanced) - -Test policies before deployment. - -| Feature | Description | Control | -|---------|-------------|---------| -| Simulate against scan | Test policy against historical scan | `CLI policy simulate` | -| Diff policies | Compare two policy outcomes | `CLI policy simulate --diff` | -| Dry-run | Preview policy effects | `CLI` | - -**CLI Usage:** -```bash -stella policy simulate --policy ./new-policy.yaml --scan scan-id-123 -stella policy simulate --policy ./new-policy.yaml --scan scan-id --diff ./old-policy.yaml -``` - -### 8.4 Policy Gates (Specialized) - -Advanced policy evaluation gates. - -| Gate | Description | Control | -|------|-------------|---------| -| Quality Gate | Block deploy based on thresholds | `Config` | -| Approval Gate | Require human approval | `Config` `UI` | -| Exception Gate | Manage temporary exceptions | `Config` `UI` | -| Stability Damping | Prevent gate flickering | `Config` | - -### 8.5 Policy Distribution (Enhanced) - -Distribute policies across environments. - -| Feature | Description | Control | -|---------|-------------|---------| -| OCI registry push | Push policies to OCI registry | `CLI policy push` | -| OCI registry pull | Pull policies from registry | `CLI policy pull` | -| Offline bundle export | Export for air-gapped environments | `CLI policy export-bundle` | -| Environment overrides | Apply environment-specific overrides | `Config` | - -**CLI Usage:** -```bash -stella policy push --policy ./policy.yaml --to registry.io/policies/prod:1.0 -stella policy pull --from registry.io/policies/prod:1.0 --output ./ -stella policy export-bundle --policy ./policy.yaml --output bundle.tar.gz -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Filter by severity | Show only specific severity levels | `--severity CRITICAL,HIGH` | Y | Y | Y | +| Minimum severity | Set minimum severity threshold | `--min-severity HIGH` | Y | Y | Y | +| Fixable only | Show only vulns with available fixes | `--fixable` | Y | Y | Y | +| Unfixed only | Show only vulns without fixes | `--unfixed` | Y | Y | Y | +| Filter by package | Filter by package name pattern | `--package "log4j*"` | Y | Y | Y | +| Filter by CVE | Filter by CVE ID pattern | `--cve "CVE-2024-*"` | Y | Y | Y | +| Filter by CWE | Filter by CWE category | `--cwe CWE-79` | Y | Y | Y | +| Filter by ecosystem | Filter by package ecosystem | `--ecosystem npm,maven` | Y | Y | Y | +| Ignore file support | Suppress findings via .stellaignore | Create `.stellaignore` file | Y | Y | Y | +| Ignore by CVE | Ignore specific CVEs | Add to `.stellaignore` | Y | Y | Y | +| Ignore by package | Ignore specific packages | Add to `.stellaignore` | Y | Y | Y | +| Ignore with expiration | Time-limited ignores | Add expiry in `.stellaignore` | - | Y | Y | +| Ignore with justification | Document ignore reasons | Add reason in `.stellaignore` | Y | Y | Y | +| Exit code on vulns | Return non-zero exit code | `--exit-code-if-vuln 1` | Y | Y | Y | +| Exit code thresholds | Exit code based on severity count | `--exit-code-if-critical 2` | Y | Y | Y | +| Fail on unknowns | Fail when unknowns exceed threshold | `--fail-on-unknowns 5%` | - | Y | Y | --- -# Part III: Specialized Capabilities +## 9. VEX Processing -## 9. Determinism & Reproducibility - -### 9.1 Replay Manifests (Specialized) - -Capture everything needed to reproduce a scan. - -| Feature | Description | Control | -|---------|-------------|---------| -| SRM generation | Generate Scan Replay Manifest | `CLI --srm-out` | -| Replay execution | Replay scan from manifest | `CLI replay` | -| Determinism verification | Verify replay matches original | `CLI replay verify` | - -**CLI Usage:** -```bash -# Generate replay manifest -stella scan --image myapp --srm-out manifest.yaml - -# Replay later -stella replay --manifest manifest.yaml --assert-digest sha256:abc... -``` - -### 9.2 Knowledge Snapshots (Specialized) - -Frozen point-in-time vulnerability knowledge. - -| Feature | Description | Control | -|---------|-------------|---------| -| Snapshot export | Export frozen knowledge state | `CLI airgap export` | -| Snapshot import | Import knowledge snapshot | `CLI airgap import` | -| Snapshot diff | Compare two snapshots | `CLI airgap diff` | -| Staleness tracking | Track snapshot age | `CLI airgap status` | - -**CLI Usage:** -```bash -stella airgap export --output knowledge-2024-01.tar.gz --sign -stella airgap import knowledge-2024-01.tar.gz --verify-only -stella airgap diff --base old.tar.gz --target new.tar.gz -stella airgap status -``` - -### 9.3 Verdict Replay (Specialized) - -Replay policy decisions for audit. - -| Feature | Description | Control | -|---------|-------------|---------| -| Snapshot replay | Replay using knowledge snapshot | `CLI replay snapshot` | -| Verdict comparison | Compare replayed vs original verdict | `CLI` | -| Drift detection | Detect verdict drift | `CLI` | - -**CLI Usage:** -```bash -stella replay snapshot --verdict -stella replay snapshot --artifact sha256:... --snapshot -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| OpenVEX ingestion | Import OpenVEX documents | `stella vex import --file vex.json` | Y | Y | Y | +| CycloneDX VEX ingestion | Import CycloneDX VEX documents | `stella vex import --file cdx-vex.json` | Y | Y | Y | +| CSAF ingestion | Import CSAF advisories | `stella vex import --file csaf.json` | Y | Y | Y | +| VEX auto-detection | Detect VEX format automatically | Automatic on import | Y | Y | Y | +| VEX validation | Validate VEX document structure | Automatic on import | Y | Y | Y | +| VEX status: not_affected | Apply not_affected status | Suppresses finding | Y | Y | Y | +| VEX status: affected | Apply affected status | Surfaces finding | Y | Y | Y | +| VEX status: fixed | Apply fixed status | Adds fix context | Y | Y | Y | +| VEX status: under_investigation | Apply investigation status | Marks as Unknown | Y | Y | Y | +| VEX justification tracking | Track VEX justifications | Automatic | Y | Y | Y | +| VEX impact statement | Include impact statements | Automatic | Y | Y | Y | +| VEX action statement | Include action statements | Automatic | Y | Y | Y | +| Multi-issuer VEX | Ingest VEX from multiple issuers | Multiple imports | - | Y | Y | +| VEX issuer trust levels | Assign trust weights to issuers | Configure Issuer Directory | - | Y | Y | +| VEX consensus engine | Compute consensus from multiple VEX | Automatic via VexLens | - | - | Y | +| K4 lattice logic | Use four-valued logic for consensus | Automatic | - | - | Y | +| VEX conflict detection | Detect conflicting VEX statements | Automatic | - | - | Y | +| VEX conflict surfacing | Surface conflicts in output | Automatic | - | - | Y | +| Issuer Directory | Manage trusted VEX issuers | Configure in `issuer-directory.yaml` | - | Y | Y | +| CSAF publisher discovery | Discover CSAF publishers | Configure discovery | - | - | Y | +| VEX export | Export VEX from scan results | `stella vex export --scan ` | Y | Y | Y | +| VEX generation | Generate VEX for findings | `stella vex generate` | - | Y | Y | --- -## 10. Attestation & Signing +## 10. Reachability Analysis -### 10.1 DSSE Attestation (Specialized) - -in-toto DSSE attestations for evidence. - -| Feature | Description | Control | -|---------|-------------|---------| -| SBOM attestation | Sign SBOMs with DSSE | `CLI attest` | -| Verdict attestation | Sign policy verdicts | `CLI` | -| Evidence bundles | Create signed evidence bundles | `CLI` | - -### 10.2 Keyless Signing (Specialized) - -Sigstore-compatible keyless signing. - -| Feature | Description | Control | -|---------|-------------|---------| -| Keyless sign | Sign using OIDC identity | `CLI sign keyless` | -| Rekor upload | Upload to transparency log | `CLI sign keyless --rekor` | -| Verify keyless | Verify keyless signatures | `CLI sign verify-keyless` | -| Self-hosted Sigstore | Use self-hosted Fulcio/Rekor | `Config` | - -**CLI Usage:** -```bash -stella sign keyless --input artifact.json --rekor -stella sign verify-keyless --input artifact.json --bundle artifact.sigstore -``` - -### 10.3 Regional Cryptography (Specialized) - -Sovereign cryptography profiles. - -| Profile | Algorithms | Use Case | Control | -|---------|------------|----------|---------| -| FIPS-140-3 | ECDSA P-256, RSA-PSS | US federal | `Config` | -| eIDAS | ETSI TS 119 312 | EU qualified | `Config` | -| GOST-2012 | GOST R 34.10-2012 | Russian Federation | `Config` | -| SM2 | GM/T 0003.2-2012 | PRC | `Config` | -| PQC | Dilithium, Falcon | Post-quantum | `Config` | - -**Configuration:** -```yaml -# etc/appsettings.crypto.yaml -cryptography: - profile: "fips-140-3" - algorithms: - signing: "ES256" - hashing: "SHA256" -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Static reachability | Determine code reachability via static analysis | `stella scan --reachability` | - | Y | Y | +| Call graph building | Build call graph from entry points | Automatic with reachability | - | Y | Y | +| Entry point detection | Detect application entry points | Automatic | - | Y | Y | +| Reachable classification | Mark vulns as REACHABLE | In scan output | - | Y | Y | +| Unreachable classification | Mark vulns as UNREACHABLE | In scan output | - | Y | Y | +| Unknown reachability | Mark vulns with unknown reachability | In scan output | - | Y | Y | +| Call path visualization | View call paths to vulnerable code | `stella graph show --cve ` | - | Y | Y | +| Call path export | Export call paths | `stella graph export` | - | Y | Y | +| Binary layer analysis | Analyze compiled binaries for symbols | Automatic | - | - | Y | +| Symbol presence verification | Verify vulnerable symbols exist | Automatic | - | - | Y | +| Runtime layer analysis | Confirm execution via eBPF probes | Configure runtime signals | - | - | Y | +| Three-layer proofs | Combine static + binary + runtime | Automatic when all available | - | - | Y | +| Confidence tier: Confirmed | All three layers agree | Automatic | - | - | Y | +| Confidence tier: Likely | Static + binary agree | Automatic | - | - | Y | +| Confidence tier: Present | Package present, no path evidence | Automatic | - | Y | Y | +| Signed reachability graphs | Sign reachability graphs with DSSE | Configure in `attestor.yaml` | - | - | Y | +| Edge-bundle attestation | Sign individual path edges | Configure in `attestor.yaml` | - | - | Y | +| Reachability proof export | Export reachability proofs | `stella graph export --proof` | - | - | Y | --- -## 11. Offline Operations +## 11. Secrets Detection -### 11.1 Offline Update Kits (Specialized) - -Bundle everything for air-gapped environments. - -| Feature | Description | Control | -|---------|-------------|---------| -| Kit export | Export complete offline bundle | `CLI offline export` | -| Kit import | Import offline bundle | `CLI offline import` | -| Kit verification | Verify bundle integrity | `CLI` | -| Staleness policy | Configure max bundle age | `Config` | - -**Configuration:** -```yaml -# etc/airgap.yaml -staleness: - maxAgeHours: 168 # 7 days - warnAgeHours: 72 # 3 days - staleAction: block # block or warn - -import: - verifySignature: true - verifyMerkleRoot: true - enforceMonotonicity: true -``` - -### 11.2 Mirror Services (Specialized) - -Local mirrors for vulnerability feeds. - -| Feature | Description | Control | -|---------|-------------|---------| -| Feed mirror | Mirror advisory feeds locally | `Config` | -| Registry mirror | Mirror container registry | `Config` | -| Transparency mirror | Mirror Rekor transparency log | `Config` | - -### 11.3 Egress Control (Specialized) - -Network access control for sealed mode. - -| Feature | Description | Control | -|---------|-------------|---------| -| Allowlist mode | Only allow specified hosts | `Config` | -| Denylist mode | Block specified hosts | `Config` | -| Localhost only | Fully sealed operation | `Config` | - -**Configuration:** -```yaml -# etc/airgap.yaml -egressPolicy: - mode: allowlist - allowedHosts: [] - allowLocalhost: true -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Credential leak detection | Scan for accidentally committed secrets | `stella scan --secrets` | Coming | Coming | Coming | +| AWS access key detection | Detect AWS access keys | Automatic with secrets scan | Coming | Coming | Coming | +| AWS secret key detection | Detect AWS secret access keys | Automatic | Coming | Coming | Coming | +| GitHub token detection | Detect GitHub personal access tokens | Automatic | Coming | Coming | Coming | +| GitLab token detection | Detect GitLab tokens | Automatic | Coming | Coming | Coming | +| Private key detection | Detect private keys (RSA, EC, etc.) | Automatic | Coming | Coming | Coming | +| Database credential detection | Detect database connection strings | Automatic | Coming | Coming | Coming | +| API key detection | Detect common API keys | Automatic | Coming | Coming | Coming | +| JWT secret detection | Detect JWT signing secrets | Automatic | Coming | Coming | Coming | +| Generic high-entropy strings | Detect high-entropy secrets | Automatic | Coming | Coming | Coming | +| Rule bundle management | Manage detection rule bundles | `stella secrets bundle` | Coming | Coming | Coming | +| Built-in rule bundles | Use shipped rule bundles | Automatic | Coming | Coming | Coming | +| Custom rule bundles | Create custom rule bundles | `stella secrets bundle create` | Coming | - | Coming | +| Rule bundle signing | Sign rule bundles | `stella secrets bundle create --sign` | Coming | - | Coming | +| Rule bundle verification | Verify rule bundle integrity | `stella secrets bundle verify` | Coming | Coming | Coming | +| Masked output | Mask detected secrets in output | Automatic | Coming | Coming | Coming | +| Secret location reporting | Report file and line of secrets | In scan output | Coming | Coming | Coming | +| Secrets in policy | Use secrets findings in policy rules | `secret.hasFinding()` predicate | Coming | - | Coming | +| Secrets severity levels | Assign severity to secret types | In rule definitions | Coming | Coming | Coming | +| Secrets confidence levels | Assign confidence to detections | In rule definitions | Coming | Coming | Coming | --- -## 12. Risk Scoring +## 12. Policy Engine -### 12.1 CVSS Scoring (Enhanced) - -CVSS-based risk assessment. - -| Feature | Description | Control | -|---------|-------------|---------| -| CVSS v4.0 | Full CVSS 4.0 support | Automatic | -| Environmental metrics | Apply environmental context | `Config` | -| Temporal metrics | Apply temporal context | Automatic | -| CVSS receipts | Signed CVSS calculations | `CLI` | - -**CLI Usage:** -```bash -stella cvss score --vuln CVE-2024-1234 --policy cvss-policy.json --vector "CVSS:4.0/..." -stella cvss show --receipt -``` - -### 12.2 Risk Budgets (Enhanced) - -Track risk across portfolios. - -| Feature | Description | Control | -|---------|-------------|---------| -| Risk budget definition | Define acceptable risk levels | `Config` | -| Budget tracking | Track consumption over time | `UI` | -| Budget alerts | Alert when budget exceeded | `Config` | - -### 12.3 Unknowns Tracking (Specialized) - -Track and manage unknown components. - -| Feature | Description | Control | -|---------|-------------|---------| -| Unknown detection | Detect unidentified components | Automatic | -| Unknown classification | Hot/Warm/Cold/Resolved bands | Automatic | -| Decay tracking | Track uncertainty over time | Automatic | -| Budget enforcement | Fail if unknowns exceed threshold | `Config` | - -**CLI Usage:** -```bash -stella unknowns list -stella unknowns show --id -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Policy pack support | Define policies as reusable packs | Create policy YAML files | Y | Y | Y | +| Starter-day1 pack | Production-ready starter policy | `stella policy install starter-day1` | Y | Y | Y | +| Custom policy creation | Create custom policy packs | Write policy YAML | Y | Y | Y | +| Policy validation | Validate policy syntax | `stella policy validate --path policy.yaml` | Y | Y | Y | +| Severity-based rules | Block/warn based on severity | Define severity rules | Y | Y | Y | +| Reachability-based rules | Block/warn based on reachability | Define reachability rules | - | Y | Y | +| VEX-based rules | Allow VEX-suppressed findings | Define VEX bypass rules | Y | Y | Y | +| CVSS-based rules | Rules based on CVSS scores | Define CVSS threshold rules | Y | Y | Y | +| EPSS-based rules | Rules based on EPSS scores | Define EPSS threshold rules | - | Y | Y | +| KEV-based rules | Block KEV vulnerabilities | Define KEV rules | Y | Y | Y | +| Package-based rules | Rules for specific packages | Define package rules | Y | Y | Y | +| Ecosystem-based rules | Rules for specific ecosystems | Define ecosystem rules | Y | Y | Y | +| Age-based rules | Rules based on CVE age | Define age threshold rules | - | Y | Y | +| Fix-available rules | Rules requiring fixes to exist | Define fix-required rules | Y | Y | Y | +| Unknowns budget | Fail when unknowns exceed threshold | `unknownsBudget: 5%` | - | Y | Y | +| Policy simulation | Test policy against historical scans | `stella policy simulate` | - | Y | Y | +| Policy diff | Compare two policy outcomes | `stella policy simulate --diff` | - | Y | Y | +| Policy dry-run | Preview policy effects | `--dry-run` flag | - | Y | Y | +| Policy push to OCI | Push policies to OCI registry | `stella policy push --to registry/policy:v1` | - | Y | Y | +| Policy pull from OCI | Pull policies from OCI registry | `stella policy pull --from registry/policy:v1` | - | Y | Y | +| Policy list packs | List available policy packs | `stella policy list-packs` | Y | Y | Y | +| Policy export bundle | Export policy for offline use | `stella policy export-bundle` | - | - | Y | +| Policy import bundle | Import offline policy bundle | `stella policy import-bundle` | - | - | Y | +| Policy inheritance | Inherit from base policies | Define `extends` in policy | - | Y | Y | +| Policy overrides | Override inherited rules | Define overrides | - | Y | Y | +| Environment-specific policies | Different policies per environment | Define env-specific rules | - | Y | Y | --- -# Part IV: Platform Features +## 13. Policy Gates -## 13. Authentication & Authorization - -### 13.1 Authentication Methods (Base) - -User and service authentication. - -| Method | Description | Control | -|--------|-------------|---------| -| OAuth 2.0 | Authorization code flow | `Config` | -| Client credentials | Service-to-service auth | `Config` | -| DPoP | Demonstrating Proof of Possession | `Config` | -| mTLS | Mutual TLS authentication | `Config` | - -**Configuration:** -```yaml -# etc/authority.yaml -clients: - - clientId: "scanner-service" - grantTypes: ["client_credentials"] - scopes: ["scan:read", "scan:write"] - senderConstraint: "dpop" -``` - -### 13.2 Role-Based Access (Base) - -Predefined roles and permissions. - -| Role | Permissions | -|------|-------------| -| policy-author | Create and edit policies | -| policy-reviewer | Review policy changes | -| policy-approver | Approve policy promotion | -| export-viewer | View export results | -| export-operator | Trigger exports | -| airgap-operator | Import/export offline kits | - -### 13.3 Service Accounts (Enhanced) - -Automated service identities. - -| Feature | Description | Control | -|---------|-------------|---------| -| Service accounts | Define service identities | `Config` | -| Delegated tokens | Issue delegated access tokens | `Config` | -| ABAC attributes | Attribute-based access control | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Quality gate | Block/warn based on thresholds | Configure quality gate rules | Y | Y | Y | +| Approval gate | Require human approval | Configure approval workflows | - | - | Y | +| Exception gate | Manage temporary exceptions | Request exceptions via UI/API | - | - | Y | +| Exception expiration | Auto-expire exceptions | Set expiration in exception | - | - | Y | +| Exception justification | Require justification for exceptions | Mandatory field | - | - | Y | +| Exception approval routing | Route to appropriate approvers | Configure routing templates | - | - | Y | +| Stability damping | Prevent gate flickering | Configure `StabilityDampingGate` | - | - | Y | +| Progressive rollout | Gradual policy enforcement | Configure rollout percentage | - | - | Y | +| Gate bypass for emergencies | Emergency bypass mechanism | Requires elevated permissions | - | - | Y | +| Gate audit trail | Log all gate decisions | Automatic | - | Y | Y | --- -## 14. Deployment & Operations +## 14. Risk Scoring -### 14.1 Deployment Options (Base) - -Platform deployment configurations. - -| Option | Description | -|--------|-------------| -| Docker Compose | Single-node development/test | -| Kubernetes/Helm | Production Kubernetes deployment | -| Air-gapped | Fully offline deployment | -| Multi-tenant | Isolated tenant deployments | - -### 14.2 Storage (Base) - -Data storage options. - -| Backend | Use Case | Control | -|---------|----------|---------| -| PostgreSQL | Primary data store (16+) | `Config` | -| Valkey/Redis | Caching and rate limiting | `Config` | - -**Configuration:** -```yaml -# etc/concelier.yaml -storage: - driver: postgres - connectionString: "Host=postgres;Database=stellaops..." - maxPoolSize: 100 - autoMigrate: false -``` - -### 14.3 Scaling (Enterprise) - -Horizontal scaling options. - -| Feature | Description | Control | -|---------|-------------|---------| -| Worker pools | Scale scan workers | `Config` | -| Queue sharding | Distribute work across queues | `Config` | -| Read replicas | Scale read operations | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| CVSS 4.0 base scoring | Calculate CVSS 4.0 base scores | Automatic | Y | Y | Y | +| CVSS environmental scoring | Apply environmental metrics | Configure CVSS policy | - | Y | Y | +| Custom risk scoring | Define custom scoring formulas | Configure in policy | - | - | Y | +| Risk budget definition | Define acceptable risk levels | Configure risk budgets | - | - | Y | +| Risk budget tracking | Track budget consumption | View in UI/API | - | - | Y | +| Risk budget alerts | Alert on budget thresholds | Configure alert thresholds | - | - | Y | +| Unknowns tracking | Track unidentified components | `stella unknowns list` | - | Y | Y | +| Unknowns classification | Classify as Hot/Warm/Cold/Resolved | Automatic | - | - | Y | +| Unknowns decay tracking | Track uncertainty over time | Automatic | - | - | Y | +| Unknowns blast radius | Estimate impact of unknowns | In analysis output | - | - | Y | +| Portfolio risk view | Aggregate risk across images | Via UI dashboard | - | - | Y | +| Risk trends | View risk trends over time | Via UI dashboard | - | - | Y | --- -## 15. Integrations +## 15. Comparison & Diff -### 15.1 CI/CD Integration (Base) - -Integrate with CI/CD pipelines. - -| Feature | Description | Control | -|---------|-------------|---------| -| Exit codes | Configurable exit codes | `CLI` | -| SARIF output | GitHub/GitLab SARIF integration | `CLI --output sarif` | -| CI templates | GitHub Actions, GitLab CI templates | `CLI ci generate` | - -**CLI Usage:** -```bash -stella ci generate --platform github > .github/workflows/scan.yml -stella ci generate --platform gitlab > .gitlab-ci.yml -``` - -### 15.2 Registry Webhooks (Enhanced) - -React to registry events. - -| Feature | Description | Control | -|---------|-------------|---------| -| Push webhook | Trigger scan on image push | `Config` | -| Admission control | Block deployment on failure | `Config` | - -### 15.3 Notifications (Enhanced) - -Alert and notification channels. - -| Channel | Description | Control | -|---------|-------------|---------| -| Slack | Slack webhook integration | `Config` | -| Microsoft Teams | Teams webhook integration | `Config` | -| Email | SMTP email notifications | `Config` | -| Webhooks | Generic webhook integration | `Config` | -| PagerDuty | PagerDuty incident integration | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| SBOM comparison | Compare two SBOMs | `stella compare sbom --a v1.json --b v2.json` | Y | Y | Y | +| Package diff | Show added/removed packages | In comparison output | Y | Y | Y | +| Version diff | Show version changes | In comparison output | Y | Y | Y | +| License diff | Show license changes | In comparison output | Y | Y | Y | +| Vulnerability diff | Show vuln changes between scans | `stella compare scan --a --b ` | Y | Y | Y | +| New vulnerabilities | Show newly introduced vulns | In comparison output | Y | Y | Y | +| Fixed vulnerabilities | Show fixed/removed vulns | In comparison output | Y | Y | Y | +| Semantic risk delta | Compare security meaning, not counts | `stella compare risk` | - | - | Y | +| Reachability drift | Detect reachability changes | `stella drift reachability` | - | - | Y | +| Policy outcome diff | Compare policy decisions | `stella policy simulate --diff` | - | Y | Y | +| Smart-Diff summary | "Exploitability dropped 40%" style | In comparison output | - | - | Y | --- -## 16. Observability +## 16. Deterministic Replay -### 16.1 Telemetry (Base) - -OpenTelemetry-based observability. - -| Feature | Description | Control | -|---------|-------------|---------| -| Structured logging | JSON structured logs | `Config` | -| Tracing | Distributed tracing via OTLP | `Config` | -| Metrics | Prometheus-compatible metrics | `Config` | - -**Configuration:** -```yaml -# etc/concelier.yaml -telemetry: - enabled: true - enableTracing: true - enableMetrics: true - otlpEndpoint: "http://otel-collector:4317" - serviceName: "stellaops-scanner" -``` - -### 16.2 Timeline Indexer (Enhanced) - -Historical event tracking. - -| Feature | Description | Control | -|---------|-------------|---------| -| Event indexing | Index security events | Automatic | -| Timeline queries | Query event history | `API` `UI` | -| Audit trail | Complete audit log | `UI` | - -### 16.3 Evidence Locker (Specialized) - -Sealed evidence storage. - -| Feature | Description | Control | -|---------|-------------|---------| -| Evidence sealing | Create tamper-evident evidence | `API` | -| Legal hold | Apply legal holds to evidence | `API` `UI` | -| Retention policies | Configure retention periods | `Config` | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Scan Replay Manifest (SRM) | Generate manifest for replay | `stella scan --srm-out manifest.yaml` | - | - | Y | +| Replay scan from manifest | Replay using SRM | `stella replay --manifest manifest.yaml` | - | - | Y | +| Replay digest assertion | Verify replay matches original | `stella replay --assert-digest sha256:...` | - | - | Y | +| Knowledge snapshot export | Export frozen knowledge state | `stella airgap export --output snapshot.tar.gz` | - | - | Y | +| Knowledge snapshot import | Import knowledge snapshot | `stella airgap import snapshot.tar.gz` | - | - | Y | +| Knowledge snapshot diff | Compare two snapshots | `stella airgap diff --base a.tar.gz --target b.tar.gz` | - | - | Y | +| Staleness tracking | Track snapshot age | `stella airgap status` | - | - | Y | +| Staleness warnings | Warn when snapshot is aging | Automatic | - | - | Y | +| Staleness blocking | Block when snapshot too old | Configure `staleAction: block` | - | - | Y | +| Verdict replay | Replay policy decisions | `stella replay snapshot --verdict ` | - | - | Y | +| Replay verification | Verify replay produces same result | Automatic with assertion | - | - | Y | +| Feed snapshot inclusion | Include feed snapshots in replay | Automatic | - | - | Y | +| Analyzer version pinning | Pin analyzer versions for replay | In SRM | - | - | Y | +| Policy version pinning | Pin policy version for replay | In SRM | - | - | Y | --- -# Appendices +## 17. Attestation & Signing -## Appendix A: CLI Command Reference - -### Top-Level Commands - -| Command | Description | -|---------|-------------| -| `stella scan` | Scan container images for vulnerabilities | -| `stella replay` | Replay scans from manifests | -| `stella policy` | Policy management commands | -| `stella airgap` | Air-gap operations | -| `stella sign` | Signing operations | -| `stella verify` | Verification operations | -| `stella vex` | VEX management | -| `stella graph` | Reachability graph operations | -| `stella compare` | Comparison operations | -| `stella ci` | CI/CD integration | -| `stella unknowns` | Unknown component tracking | -| `stella cvss` | CVSS scoring operations | - -### Scan Command Options - -```bash -stella scan [options] - --image Container image to scan - --sbom Use existing SBOM instead of image - --rootfs Scan extracted filesystem - --output Output format: table, json, sarif, cyclonedx - --severity Filter by severity: CRITICAL,HIGH,MEDIUM,LOW - --fixable Show only vulnerabilities with fixes - --reachability Enable reachability analysis - --sbom-out Export SBOM to file - --srm-out Export replay manifest - --exit-code-if-vuln Exit code when vulnerabilities found -``` - -### Policy Command Options - -```bash -stella policy [options] - validate --path Validate policy YAML - install Install policy pack - list-packs List available policy packs - simulate --policy --scan Simulate policy - push --policy --to Push to OCI registry - pull --from Pull from OCI registry - export-bundle --policy --output - import-bundle --bundle -``` - -### Air-Gap Command Options - -```bash -stella airgap [options] - export --output Export knowledge snapshot - --include-advisories Include advisory feeds - --include-vex Include VEX statements - --include-policies Include policy bundles - --sign Sign the manifest - import Import knowledge snapshot - --verify-only Verify without applying - --force Force import despite staleness - diff --base --target Compare snapshots - status Show staleness status -``` +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| DSSE attestation format | Use DSSE envelope format | Automatic | - | Y | Y | +| in-toto attestation | Generate in-toto attestations | Configure Attestor | - | Y | Y | +| SBOM attestation | Sign SBOMs with attestation | `stella attest sbom` | - | Y | Y | +| Scan result attestation | Sign scan results | `stella attest scan` | - | Y | Y | +| Verdict attestation | Sign policy verdicts | `stella attest verdict` | - | - | Y | +| Evidence bundle creation | Create signed evidence bundles | `stella evidence bundle` | - | - | Y | +| Keyless signing | Sign using OIDC identity (Sigstore) | `stella sign keyless --input file` | - | Y | Y | +| Rekor transparency log | Upload to Rekor | `stella sign keyless --rekor` | - | Y | Y | +| Keyless verification | Verify keyless signatures | `stella sign verify-keyless` | - | Y | Y | +| Self-hosted Fulcio | Use self-hosted Fulcio | Configure `--fulcio-url` | - | - | Y | +| Self-hosted Rekor | Use self-hosted Rekor | Configure `--rekor-url` | - | - | Y | +| Traditional key signing | Sign with managed keys | `stella sign --key-id ` | - | Y | Y | +| Key rotation support | Rotate signing keys | Via key management | - | - | Y | +| Multi-signature support | Sign with multiple keys | Configure multiple signers | - | - | Y | +| Signature verification | Verify signatures | `stella verify signature` | - | Y | Y | +| Attestation verification | Verify attestations | `stella verify attestation` | - | Y | Y | --- -## Appendix B: Configuration Reference +## 18. Cryptography Profiles -### Configuration Files - -| File | Purpose | -|------|---------| -| `etc/concelier.yaml` | Advisory ingestion configuration | -| `etc/authority.yaml` | Authentication and authorization | -| `etc/airgap.yaml` | Air-gap operations | -| `etc/scanner.yaml` | Scanner configuration | -| `etc/appsettings.crypto.*.yaml` | Cryptography profiles | - -### Environment Variables - -| Variable | Description | -|----------|-------------| -| `STELLAOPS_BACKEND_URL` | Backend API URL | -| `GITHUB_PAT` | GitHub Personal Access Token for GHSA | -| `NVD_API_KEY` | NVD API key for enhanced rate limits | -| `STELLAOPS_KMS_PASSPHRASE` | KMS key passphrase | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Default crypto profile | Standard cryptographic algorithms | Default | Y | Y | Y | +| FIPS-140-3 profile | US federal crypto requirements | Configure `profile: fips-140-3` | - | - | Y | +| eIDAS profile | EU qualified signatures | Configure `profile: eidas` | - | - | Y | +| GOST-2012 profile | Russian Federation requirements | Configure `profile: gost-2012` | - | - | Y | +| SM2 profile | PRC cryptographic requirements | Configure `profile: sm2` | - | - | Y | +| Post-quantum profile | Dilithium, Falcon algorithms | Configure `profile: pqc` | - | - | Y | +| Algorithm selection | Choose specific algorithms | Configure `algorithms` section | - | - | Y | +| Multi-profile signing | Sign with multiple profiles | Configure multiple profiles | - | - | Y | +| Profile validation | Validate crypto configuration | Automatic on startup | - | - | Y | +| Hardware security module | HSM integration | Configure HSM provider | - | - | Y | --- -## Appendix C: API Reference +## 19. Offline & Air-Gap -### REST API Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/api/v1/scans` | POST | Trigger new scan | -| `/api/v1/scans/{id}` | GET | Get scan results | -| `/api/v1/sboms` | POST | Import SBOM | -| `/api/v1/sboms/{id}` | GET | Get SBOM | -| `/api/v1/vex` | POST | Import VEX statement | -| `/api/v1/policies` | POST | Create policy | -| `/api/v1/policies/{id}/evaluate` | POST | Evaluate policy | -| `/api/v1/evidence` | POST | Create evidence bundle | - -### API Authentication - -All API requests require authentication via: -- Bearer token (OAuth 2.0 access token) -- DPoP proof header for high-security operations -- mTLS client certificate for service-to-service +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Offline Update Kit export | Export complete offline bundle | `stella airgap export --output kit.tar.gz` | - | - | Y | +| Offline Update Kit import | Import offline bundle | `stella airgap import kit.tar.gz` | - | - | Y | +| Kit signature verification | Verify kit signatures on import | Automatic or `--verify-only` | - | - | Y | +| Kit Merkle root verification | Verify kit integrity via Merkle root | Automatic | - | - | Y | +| Advisory feed inclusion | Include advisory feeds in kit | `--include-advisories` | - | - | Y | +| VEX document inclusion | Include VEX statements in kit | `--include-vex` | - | - | Y | +| Policy bundle inclusion | Include policy bundles in kit | `--include-policies` | - | - | Y | +| Trust root inclusion | Include trust roots in kit | Automatic | - | - | Y | +| Staleness policy configuration | Configure max bundle age | Configure in `airgap.yaml` | - | - | Y | +| Staleness warning threshold | Warn when bundle ages | Configure `warnAgeHours` | - | - | Y | +| Staleness block threshold | Block when bundle too old | Configure `maxAgeHours` | - | - | Y | +| Version monotonicity | Prevent rollback attacks | `enforceMonotonicity: true` | - | - | Y | +| Feed mirror service | Mirror advisory feeds locally | Deploy Mirror service | - | - | Y | +| Registry mirror support | Use registry mirrors | Configure mirrors in `scanner.yaml` | - | Y | Y | +| Transparency log mirror | Mirror Rekor transparency log | Deploy Rekor mirror | - | - | Y | +| Egress allowlist mode | Only allow specified hosts | Configure `egressPolicy.mode: allowlist` | - | - | Y | +| Egress denylist mode | Block specified hosts | Configure `egressPolicy.mode: denylist` | - | - | Y | +| Sealed mode | Block all network access | Configure sealed mode | - | - | Y | +| Localhost-only mode | Allow only localhost traffic | Configure `allowLocalhost: true` | - | - | Y | +| Time anchor (Roughtime) | Secure time from Roughtime servers | Configure Roughtime servers | - | - | Y | +| Time anchor (RFC 3161) | Secure time from TSA servers | Configure TSA servers | - | - | Y | --- -## Version Information +## 20. Verification -| Component | Version | -|-----------|---------| -| Document Version | 2.0.0 | -| Last Updated | 2026-01-04 | -| Platform Version | 2026.01 | +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Offline evidence verification | Verify evidence without network | `stella verify offline --evidence-dir ./evidence` | - | - | Y | +| Image attestation verification | Verify image has required attestations | `stella verify image registry/app@sha256:...` | - | Y | Y | +| Require SBOM attestation | Require SBOM attestation | `--require sbom` | - | Y | Y | +| Require VEX attestation | Require VEX attestation | `--require vex` | - | Y | Y | +| Require decision attestation | Require policy decision attestation | `--require decision` | - | - | Y | +| Require approval attestation | Require approval attestation | `--require approval` | - | - | Y | +| Strict mode | Fail if any attestation missing | `--strict` | - | Y | Y | +| Evidence bundle verification | Verify complete evidence bundle | `stella verify bundle --bundle ./bundle` | - | - | Y | +| Skip replay verification | Verify only input hashes | `--skip-replay` | - | - | Y | +| Trust policy application | Apply trust policy during verification | `--trust-policy policy.yaml` | - | - | Y | +| Certificate verification | Verify signing certificates | Automatic | - | Y | Y | +| Certificate chain validation | Validate full certificate chain | Automatic | - | Y | Y | +| OCSP checking | Check certificate revocation | Automatic when online | - | Y | Y | +| CRL checking | Check certificate revocation lists | Automatic | - | Y | Y | +| Offline revocation checking | Check revocation without network | Using embedded CRLs | - | - | Y | --- -*For the latest information, see the online documentation at https://docs.stella-ops.org* +## 21. Authentication + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| OAuth 2.0 authorization code | Authorization code flow for users | Configure Authority | - | Y | Y | +| OAuth 2.0 client credentials | Client credentials for services | Configure Authority | - | Y | Y | +| OAuth 2.0 refresh tokens | Refresh token support | Configure Authority | - | Y | Y | +| OpenID Connect | OIDC authentication | Configure Authority | - | Y | Y | +| DPoP (Proof of Possession) | Bind tokens to client keys | Configure `senderConstraint: dpop` | - | - | Y | +| mTLS authentication | Mutual TLS for service auth | Configure mTLS | - | - | Y | +| API key authentication | Simple API key auth | Configure API keys | Y | Y | Y | +| Token lifetime configuration | Configure token expiration | Configure in `authority.yaml` | - | Y | Y | +| Token refresh configuration | Configure refresh token lifetime | Configure in `authority.yaml` | - | Y | Y | +| LDAP integration | Authenticate via LDAP | Deploy LDAP plugin | - | - | Y | +| SAML integration | Authenticate via SAML | Deploy SAML plugin | - | - | Y | +| External IdP integration | Use external identity provider | Configure OIDC provider | - | Y | Y | +| MFA requirement | Require multi-factor auth | Configure in Authority | - | - | Y | +| Session management | Manage user sessions | Via Authority | - | Y | Y | +| Token revocation | Revoke access tokens | Via Authority API | - | Y | Y | + +--- + +## 22. Authorization & Access Control + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Role-based access control | Assign roles to users | Configure in Authority | - | Y | Y | +| policy-author role | Create and edit policies | Assign role | - | Y | Y | +| policy-reviewer role | Review policy changes | Assign role | - | Y | Y | +| policy-approver role | Approve policies for production | Assign role | - | - | Y | +| policy-operator role | Run and activate policies | Assign role | - | Y | Y | +| policy-auditor role | Audit policy decisions | Assign role | - | - | Y | +| airgap-viewer role | View offline kit status | Assign role | - | - | Y | +| airgap-operator role | Import/export offline kits | Assign role | - | - | Y | +| airgap-admin role | Full air-gap administration | Assign role | - | - | Y | +| vuln-viewer role | View vulnerability findings | Assign role | - | Y | Y | +| vuln-investigator role | Investigate and triage findings | Assign role | - | Y | Y | +| vuln-operator role | Take action on findings | Assign role | - | Y | Y | +| vuln-auditor role | Audit vulnerability decisions | Assign role | - | - | Y | +| export-viewer role | View export results | Assign role | - | Y | Y | +| export-operator role | Trigger exports | Assign role | - | Y | Y | +| export-admin role | Manage export configuration | Assign role | - | - | Y | +| notify-viewer role | View notifications | Assign role | - | Y | Y | +| notify-operator role | Manage notifications | Assign role | - | Y | Y | +| notify-admin role | Full notification admin | Assign role | - | - | Y | +| Custom roles | Define custom roles | Configure in Authority | - | - | Y | +| Attribute-based access | Fine-grained ABAC | Configure attributes | - | - | Y | +| Environment restrictions | Restrict access by environment | Configure env attributes | - | - | Y | +| Business tier restrictions | Restrict by business tier | Configure tier attributes | - | - | Y | +| Service accounts | Create service identities | Configure in Authority | - | Y | Y | +| Delegated tokens | Issue delegated access tokens | Via Authority API | - | - | Y | +| Scope-based permissions | Permission scopes on tokens | Configure scopes | - | Y | Y | + +--- + +## 23. Evidence Management + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Evidence Locker | Store tamper-evident evidence | Via EvidenceLocker API | - | - | Y | +| Evidence sealing | Seal evidence with hashes | Automatic | - | - | Y | +| Evidence retrieval | Retrieve stored evidence | Via EvidenceLocker API | - | - | Y | +| Legal hold | Apply legal hold to evidence | Via UI/API | - | - | Y | +| Legal hold override | Prevent deletion during hold | Automatic | - | - | Y | +| Retention policies | Configure retention periods | Configure policies | - | - | Y | +| Per-type retention | Different retention by type | Configure policies | - | - | Y | +| Evidence export | Export evidence bundles | Via ExportCenter | - | - | Y | +| Evidence chain verification | Verify evidence chain integrity | Via verification APIs | - | - | Y | + +--- + +## 24. Observability + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Structured JSON logging | JSON formatted log output | Configure logging | Y | Y | Y | +| Log level configuration | Set minimum log level | Configure `minimumLogLevel` | Y | Y | Y | +| Console log output | Log to console | `exportConsole: true` | Y | Y | Y | +| OpenTelemetry tracing | Distributed tracing | Configure `enableTracing: true` | - | Y | Y | +| OpenTelemetry metrics | Prometheus-compatible metrics | Configure `enableMetrics: true` | - | Y | Y | +| OTLP export | Export to OTLP collector | Configure `otlpEndpoint` | - | Y | Y | +| Custom resource attributes | Add custom trace attributes | Configure `resourceAttributes` | - | Y | Y | +| Service name configuration | Set service name for traces | Configure `serviceName` | - | Y | Y | +| Timeline event indexing | Index security events | Automatic via TimelineIndexer | - | - | Y | +| Timeline queries | Query event history | Via Timeline API | - | - | Y | +| Audit trail | Complete action audit log | Automatic | - | Y | Y | +| Audit log export | Export audit logs | Via API | - | - | Y | +| Incident bridge | Bridge to incident management | Configure Incident Bridge | - | - | Y | +| Health checks | Service health endpoints | `/health` endpoint | Y | Y | Y | +| Readiness probes | Kubernetes readiness | `/ready` endpoint | Y | Y | Y | +| Liveness probes | Kubernetes liveness | `/live` endpoint | Y | Y | Y | + +--- + +## 25. Notifications + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Slack notifications | Send to Slack webhooks | Configure Slack webhook | - | Y | Y | +| Microsoft Teams notifications | Send to Teams webhooks | Configure Teams webhook | - | Y | Y | +| Email notifications | Send via SMTP | Configure SMTP settings | - | Y | Y | +| PagerDuty integration | Create PagerDuty incidents | Configure PagerDuty | - | - | Y | +| Generic webhooks | Send to custom webhooks | Configure webhook URL | - | Y | Y | +| Notification templates | Customize notification content | Configure templates | - | Y | Y | +| Severity-based routing | Route by severity level | Configure routing rules | - | Y | Y | +| Notification escalation | Escalate unacknowledged alerts | Configure escalation | - | - | Y | +| Notification acknowledgment | Acknowledge notifications | Via Notify API | - | Y | Y | +| Notification muting | Temporarily mute notifications | Configure mute windows | - | Y | Y | +| Notification rate limiting | Limit notification frequency | Configure rate limits | - | Y | Y | + +--- + +## 26. CI/CD Integration + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Exit code control | Return codes for CI/CD | `--exit-code-if-vuln 1` | Y | Y | Y | +| GitHub Actions template | Generate GitHub Actions workflow | `stella ci generate --platform github` | Y | Y | Y | +| GitLab CI template | Generate GitLab CI pipeline | `stella ci generate --platform gitlab` | Y | Y | Y | +| Azure Pipelines template | Generate Azure Pipelines | `stella ci generate --platform azure` | Y | Y | Y | +| Jenkins template | Generate Jenkinsfile | `stella ci generate --platform jenkins` | Y | Y | Y | +| SARIF for GitHub | Upload SARIF to GitHub Security | `--output sarif` | Y | Y | Y | +| SARIF for GitLab | Upload SARIF to GitLab Security | `--output sarif` | Y | Y | Y | +| PR comments | Comment scan results on PRs | Configure CI integration | - | Y | Y | +| Status checks | Update PR status checks | Configure CI integration | - | Y | Y | +| Merge blocking | Block merge on policy failure | Configure CI integration | - | Y | Y | + +--- + +## 27. Registry Integration + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Docker Hub | Pull from Docker Hub | Default | Y | Y | Y | +| GitHub Container Registry | Pull from GHCR | Authenticate with token | Y | Y | Y | +| AWS ECR | Pull from Amazon ECR | Configure ECR credentials | Y | Y | Y | +| Google GCR | Pull from Google Container Registry | Configure GCP credentials | Y | Y | Y | +| Azure ACR | Pull from Azure Container Registry | Configure Azure credentials | Y | Y | Y | +| Harbor | Pull from Harbor registry | Configure credentials | Y | Y | Y | +| JFrog Artifactory | Pull from Artifactory | Configure credentials | Y | Y | Y | +| Quay.io | Pull from Quay | Configure credentials | Y | Y | Y | +| Private registries | Pull from any private registry | Configure credentials | Y | Y | Y | +| Registry webhook (push) | Scan on image push | Configure Zastava webhook | - | Y | Y | +| Admission controller | Block deployment on failure | Deploy admission webhook | - | - | Y | +| Image signing verification | Verify image signatures | Configure signature policy | - | - | Y | + +--- + +## 28. Deployment Options + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Docker Compose | Single-node deployment | `docker compose up` | Y | Y | Y | +| Kubernetes deployment | Deploy on Kubernetes | Use Helm charts | - | Y | Y | +| Helm charts | Helm-based deployment | `helm install stellaops` | - | Y | Y | +| Air-gapped deployment | Fully offline deployment | Use Offline Kit | - | - | Y | +| Multi-tenant deployment | Isolated tenants | Configure multi-tenancy | - | - | Y | +| High availability | HA deployment patterns | Configure replication | - | - | Y | +| Horizontal scaling | Scale workers horizontally | Configure replicas | - | - | Y | +| Auto-scaling | Kubernetes HPA integration | Configure HPA | - | - | Y | + +--- + +## 29. Storage & Infrastructure + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| PostgreSQL 16+ | Primary data storage | Configure connection string | Y | Y | Y | +| PostgreSQL connection pooling | Connection pool management | Configure pool settings | Y | Y | Y | +| PostgreSQL read replicas | Scale read operations | Configure replicas | - | - | Y | +| Valkey/Redis caching | Cache layer | Configure Valkey/Redis | - | Y | Y | +| Rate limiting | API rate limiting | Configure rate limits | - | Y | Y | +| Queue management | Job queue management | Via Scheduler | - | Y | Y | +| Queue sharding | Distribute queue load | Configure sharding | - | - | Y | +| Blob storage | Store large artifacts | Configure blob storage | - | Y | Y | +| S3-compatible storage | Use S3-compatible storage | Configure S3 endpoint | - | Y | Y | + +--- + +## 30. Web UI Features + +| Feature | Description | How to Use | F | C | E | +|---------|-------------|------------|:-:|:-:|:-:| +| Dashboard | Overview dashboard | Access via browser | - | Y | Y | +| Scan results view | View scan findings | Navigate to scans | - | Y | Y | +| Vulnerability details | Detailed vuln information | Click on vulnerability | - | Y | Y | +| SBOM viewer | View SBOM contents | Navigate to SBOMs | - | Y | Y | +| Policy editor | Edit policies in UI | Navigate to policies | - | Y | Y | +| Policy simulation UI | Simulate policies in UI | Use simulation panel | - | Y | Y | +| Exception management UI | Manage exceptions | Navigate to exceptions | - | - | Y | +| Approval workflows UI | Approve in UI | Navigate to approvals | - | - | Y | +| Timeline view | View event timeline | Navigate to timeline | - | - | Y | +| Triage canvas | Visual triage interface | Navigate to triage | - | - | Y | +| Noise gating UI | Manage noise gating | Navigate to noise gating | - | - | Y | +| Risk dashboard | Portfolio risk view | Navigate to risk | - | - | Y | +| Export center UI | Configure exports | Navigate to exports | - | Y | Y | +| Notification settings | Configure notifications | Navigate to settings | - | Y | Y | +| User management | Manage users | Navigate to admin | - | - | Y | +| Tenant management | Manage tenants | Navigate to admin | - | - | Y | +| Audit log viewer | View audit logs | Navigate to audit | - | - | Y | + +--- + +## Feature Count Summary + +| Category | Total Features | Free | Community | Enterprise | +|----------|----------------|------|-----------|------------| +| Container Scanning | 14 | 10 | 13 | 14 | +| OS Package Detection | 16 | 16 | 16 | 16 | +| Language Ecosystems | 29 | 27 | 29 | 29 | +| Vulnerability Sources | 17 | 14 | 16 | 17 | +| Vulnerability Enrichment | 18 | 15 | 17 | 18 | +| SBOM Capabilities | 17 | 12 | 15 | 17 | +| Output Formats | 16 | 12 | 14 | 16 | +| Filtering | 16 | 14 | 16 | 16 | +| VEX Processing | 22 | 12 | 17 | 22 | +| Reachability | 17 | 0 | 9 | 17 | +| Secrets Detection | 20 | 0 | 0 | 20 (Coming) | +| Policy Engine | 23 | 11 | 19 | 23 | +| Policy Gates | 10 | 2 | 3 | 10 | +| Risk Scoring | 12 | 2 | 5 | 12 | +| Comparison & Diff | 11 | 6 | 8 | 11 | +| Deterministic Replay | 14 | 0 | 0 | 14 | +| Attestation & Signing | 17 | 0 | 10 | 17 | +| Cryptography Profiles | 10 | 1 | 1 | 10 | +| Offline & Air-Gap | 20 | 0 | 2 | 20 | +| Verification | 15 | 0 | 8 | 15 | +| Authentication | 15 | 2 | 10 | 15 | +| Authorization | 26 | 0 | 13 | 26 | +| Evidence Management | 9 | 0 | 0 | 9 | +| Observability | 16 | 6 | 12 | 16 | +| Notifications | 11 | 0 | 8 | 11 | +| CI/CD Integration | 10 | 8 | 10 | 10 | +| Registry Integration | 12 | 10 | 11 | 12 | +| Deployment | 8 | 2 | 4 | 8 | +| Storage & Infrastructure | 9 | 3 | 6 | 9 | +| Web UI | 17 | 0 | 10 | 17 | +| **TOTAL** | **483** | **181** | **292** | **483** | + +--- + +*Last updated: 2026-01-04* diff --git a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md index ee28e0482..d63be6403 100644 --- a/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md +++ b/docs/implplan/SPRINT_20260104_001_BE_determinism_timeprovider_injection.md @@ -65,7 +65,7 @@ | 9 | DET-009 | DONE | DET-002, DET-003 | Guild | Refactor Replay module (6 files: ReplayEngine, ReplayModels, ReplayExportModels, ReplayManifestExporter, FeedSnapshotCoordinatorService, PolicySimulationInputLock) | | 10 | DET-010 | DONE | DET-002, DET-003 | Guild | Refactor RiskEngine module (skipped - no determinism issues found) | | 11 | DET-011 | TODO | DET-002, DET-003 | Guild | Refactor Scanner module (~45+ matches remaining) | -| 12 | DET-012 | TODO | DET-002, DET-003 | Guild | Refactor Scheduler module (~20+ matches remaining) | +| 12 | DET-012 | DONE | DET-002, DET-003 | Guild | Refactor Scheduler module (WebService, Persistence, Worker projects - 30+ files updated, tests migrated to FakeTimeProvider) | | 13 | DET-013 | TODO | DET-002, DET-003 | Guild | Refactor Signer module (~89 matches remaining) | | 14 | DET-014 | DONE | DET-002, DET-003 | Guild | Refactor Unknowns module (skipped - no determinism issues found) | | 15 | DET-015 | TODO | DET-002, DET-003 | Guild | Refactor VexLens module (~76 matches remaining) | diff --git a/docs/implplan/SPRINT_20260104_004_POLICY_secret_dsl_integration.md b/docs/implplan/SPRINT_20260104_004_POLICY_secret_dsl_integration.md index b1e22fcdb..195c6d2f2 100644 --- a/docs/implplan/SPRINT_20260104_004_POLICY_secret_dsl_integration.md +++ b/docs/implplan/SPRINT_20260104_004_POLICY_secret_dsl_integration.md @@ -29,18 +29,18 @@ Extend the Policy Engine and stella-dsl with `secret.*` predicates to enable pol | # | 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 | +| 1 | PSD-001 | DONE | None | Policy Guild | Define ISecretEvidenceProvider interface | +| 2 | PSD-002 | DONE | PSD-001 | Policy Guild | Implement SecretEvidenceContext binding | +| 3 | PSD-003 | DONE | None | Policy Guild | Add secret.hasFinding() predicate | +| 4 | PSD-004 | DONE | None | Policy Guild | Add secret.bundle.version() predicate | +| 5 | PSD-005 | DONE | None | Policy Guild | Add secret.match.count() predicate | +| 6 | PSD-006 | DONE | None | Policy Guild | Add secret.mask.applied predicate | +| 7 | PSD-007 | DONE | None | Policy Guild | Add secret.path.allowlist() predicate | +| 8 | PSD-008 | DONE | PSD-003-007 | Policy Guild | Register predicates in PolicyDslRegistry | +| 9 | PSD-009 | DONE | PSD-008 | Policy Guild | Update DSL schema validation | +| 10 | PSD-010 | DONE | PSD-008 | Policy Guild | Create example policy templates | +| 11 | PSD-011 | DONE | All | Policy Guild | Add unit and integration tests | +| 12 | PSD-012 | DONE | All | Docs Guild | Update policy/dsl.md documentation | ## Task Details @@ -540,4 +540,10 @@ when secret.path.allowlist(["**/test/**", "**/fixtures/**"]) | Date | Action | Notes | |------|--------|-------| | 2026-01-04 | Sprint created | Part of secret leak detection implementation | +| 2026-01-04 | PSD-001 to PSD-008 completed | Created ISecretEvidenceProvider, SecretFinding, SecretBundleMetadata, SecretEvidenceContext, SecretSignalBinder in `src/Policy/__Libraries/StellaOps.Policy/Secrets/`. Created SecretSignalContextExtensions in PolicyDsl (moved to avoid circular dependency). | +| 2026-01-04 | PSD-009 completed | Created signals-schema@1.json, updated spl-schema@1.json with secret signal examples and new operators (matches, exists). | +| 2026-01-04 | PSD-010 completed | Created spl-secret-block@1.json and spl-secret-warn@1.json example policies. | +| 2026-01-04 | PSD-011 completed | Created unit tests in SecretEvidenceContextTests.cs, SecretSignalBinderTests.cs, SecretSignalContextExtensionsTests.cs. All 8 PolicyDsl tests pass. | +| 2026-01-04 | PSD-012 completed | Updated docs/modules/policy/secret-leak-detection-readiness.md with implemented predicates table and code examples. | +| 2026-01-04 | Sprint completed | All 12 tasks DONE. | diff --git a/docs/modules/policy/secret-leak-detection-readiness.md b/docs/modules/policy/secret-leak-detection-readiness.md index a5069b5c7..0e59f512d 100644 --- a/docs/modules/policy/secret-leak-detection-readiness.md +++ b/docs/modules/policy/secret-leak-detection-readiness.md @@ -23,20 +23,67 @@ - Rule bundles, signature manifests, and validator hash lists ship with Offline Kit; rule updates must be signed and versioned to preserve determinism. ## 3. Policy Engine considerations -- **New predicates** - - `secret.hasFinding(ruleId?, severity?, confidence?)` - - `secret.bundle.version(requiredVersion)` - - `secret.mask.applied` (bool) — verify masking for high severity hits. - - `secret.path.allowlist` — tenant-configured allow list keyed by digest/path. -- **Lattice weight suggestions** - - High severity & high confidence → escalate to `block` unless waived. - - Low confidence → default to `warn` with optional escalation when multiple matches occur (`secret.match.count >= N`). -- **Waiver workflow** - - Reuse VEX-first lattice approach: require attach of remediation note, ticket reference, and expiration date. - - Ensure waivers attach rule version so upgraded rules re-evaluate automatically. -- **Masking / privacy** - - Minimum masking: first and last 2 characters retained; remainder replaced with `*`. - - Persist masked payload only; full value never leaves scanner context. + +### 3.1 Implemented predicates (SPRINT_20260104_004_POLICY) + +The following secret-related signals are now available via `StellaOps.PolicyDsl.SignalContext`: + +| Signal | Type | Description | +|--------|------|-------------| +| `secret.has_finding` | bool | True if any secret finding exists | +| `secret.count` | int | Total number of secret findings | +| `secret.severity.critical` | bool | True if any critical severity finding exists | +| `secret.severity.high` | bool | True if any high severity finding exists | +| `secret.severity.medium` | bool | True if any medium severity finding exists | +| `secret.severity.low` | bool | True if any low severity finding exists | +| `secret.confidence.high` | bool | True if any high confidence finding exists | +| `secret.confidence.medium` | bool | True if any medium confidence finding exists | +| `secret.confidence.low` | bool | True if any low confidence finding exists | +| `secret.mask.applied` | bool | True if masking was applied to all findings | +| `secret.bundle.version` | string | Active bundle version (YYYY.MM format) | +| `secret.bundle.id` | string | Active bundle identifier | +| `secret.bundle.rule_count` | int | Number of rules in the active bundle | +| `secret.bundle.signer_key_id` | string | Key ID used to sign the bundle | +| `secret.aws.count` | int | Count of AWS-related secret findings | +| `secret.github.count` | int | Count of GitHub-related secret findings | +| `secret.private_key.count` | int | Count of private key findings | + +### 3.2 Usage in SPL policies + +```json +{ + "conditions": [ + { "field": "secret.severity.critical", "operator": "eq", "value": true }, + { "field": "secret.bundle.version", "operator": "gte", "value": "2025.01" } + ] +} +``` + +See example policies in `src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-block@1.json` and `spl-secret-warn@1.json`. + +### 3.3 Integration with SignalContext + +```csharp +using StellaOps.Policy.Secrets; +using StellaOps.PolicyDsl; + +// Add secret evidence to policy evaluation +var signalContext = SignalContext.Builder() + .WithSecretEvidence(secretEvidenceProvider) + .Build(); +``` + +### 3.4 Lattice weight suggestions +- High severity & high confidence: escalate to `block` unless waived. +- Low confidence: default to `warn` with optional escalation when multiple matches occur (`secret.match.count >= N`). + +### 3.5 Waiver workflow +- Reuse VEX-first lattice approach: require attach of remediation note, ticket reference, and expiration date. +- Ensure waivers attach rule version so upgraded rules re-evaluate automatically. + +### 3.6 Masking / privacy +- Minimum masking: first and last 2 characters retained; remainder replaced with `*`. +- Persist masked payload only; full value never leaves scanner context. ## 4. Security guardrails - Rule bundle signing: Signer issues DSSE envelope for each ruleset; Policy must verify signature before enabling new bundle. @@ -62,16 +109,18 @@ ### Decision tracker | Decision | Owner(s) | Target date | Status | | --- | --- | --- | --- | -| Masking depth (paths vs payloads) | Security Guild | 2025-11-10 | Pending — workshop aligned with Northwind demo | +| Masking depth (paths vs payloads) | Security Guild | 2025-11-10 | Pending | | Telemetry retention granularity | Policy + Observability Guild | 2025-11-10 | Pending | | Default rule bundles (cloud creds/SSH/JWT) | Security Guild | 2025-11-10 | Draft proposals under review | | Tenant override format | Policy Guild | 2025-11-10 | Pending | +| Policy predicates implementation | Policy Guild | 2026-01-04 | **DONE** (SPRINT_20260104_004_POLICY) | ## 7. Next steps -1. Policy Guild drafts predicate specs + policy templates (map to DOCS-SCANNER-BENCH-62-007 exit criteria). +1. ~~Policy Guild drafts predicate specs + policy templates~~ **DONE** — See `spl-secret-block@1.json`, `spl-secret-warn@1.json`. 2. Security Guild reviews signing + masking requirements; align with Surface.Secrets roadmap. -3. Docs Guild (this task) continues maintaining `docs/benchmarks/scanner/deep-dives/secrets.md` with finalized rule taxonomy and references. +3. Docs Guild continues maintaining `docs/benchmarks/scanner/deep-dives/secrets.md` with finalized rule taxonomy and references. 4. Engineering provides prototype fixture outputs for review once SCANNER-ENG-0007 spikes begin. +5. **NEW**: Integration testing between Scanner.Analyzers.Secrets and Policy DSL signals. ## Coordination diff --git a/src/Policy/StellaOps.PolicyDsl/SecretSignalContextExtensions.cs b/src/Policy/StellaOps.PolicyDsl/SecretSignalContextExtensions.cs new file mode 100644 index 000000000..23d9f4105 --- /dev/null +++ b/src/Policy/StellaOps.PolicyDsl/SecretSignalContextExtensions.cs @@ -0,0 +1,106 @@ +// ----------------------------------------------------------------------------- +// SecretSignalContextExtensions.cs +// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration) +// Task: PSD-008 - Register predicates in PolicyDslRegistry (via signal context) +// ----------------------------------------------------------------------------- + +using StellaOps.Policy.Secrets; + +namespace StellaOps.PolicyDsl; + +/// +/// Extension methods for integrating secret evidence with PolicyDsl SignalContext. +/// +public static class SecretSignalContextExtensions +{ + /// + /// Adds secret evidence signals to the signal context. + /// + /// The signal context. + /// The secret evidence context. + /// The signal context for chaining. + public static SignalContext WithSecretEvidence( + this SignalContext context, + SecretEvidenceContext evidenceContext) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(evidenceContext); + + // Add flat signals + var signals = SecretSignalBinder.BindToSignals(evidenceContext); + foreach (var (name, value) in signals) + { + context.SetSignal(name, value); + } + + // Add nested object for member access (secret.severity.high, etc.) + var nested = SecretSignalBinder.BindToNestedObject(evidenceContext); + context.SetSignal("secret", nested); + + return context; + } + + /// + /// Adds secret evidence signals to the signal context builder. + /// + /// The signal context builder. + /// The secret evidence context. + /// The builder for chaining. + public static SignalContextBuilder WithSecretEvidence( + this SignalContextBuilder builder, + SecretEvidenceContext evidenceContext) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(evidenceContext); + + // Add flat signals + var signals = SecretSignalBinder.BindToSignals(evidenceContext); + foreach (var (name, value) in signals) + { + builder.WithSignal(name, value); + } + + // Add nested object for member access + var nested = SecretSignalBinder.BindToNestedObject(evidenceContext); + builder.WithSignal("secret", nested); + + return builder; + } + + /// + /// Adds secret evidence signals from a provider. + /// + /// The signal context builder. + /// The secret evidence provider. + /// The builder for chaining. + public static SignalContextBuilder WithSecretEvidence( + this SignalContextBuilder builder, + ISecretEvidenceProvider provider) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(provider); + + var context = new SecretEvidenceContext(provider); + return builder.WithSecretEvidence(context); + } + + /// + /// Creates a signal context builder with secret evidence. + /// + /// The secret evidence context. + /// A new builder with secret signals. + public static SignalContextBuilder CreateBuilderWithSecrets(SecretEvidenceContext evidenceContext) + { + return SignalContext.Builder().WithSecretEvidence(evidenceContext); + } + + /// + /// Creates a signal context with secret evidence. + /// + /// The secret evidence context. + /// A new signal context with secret signals. + public static SignalContext CreateContextWithSecrets(SecretEvidenceContext evidenceContext) + { + return CreateBuilderWithSecrets(evidenceContext).Build(); + } +} diff --git a/src/Policy/__Libraries/StellaOps.Policy/Schemas/signals-schema@1.json b/src/Policy/__Libraries/StellaOps.Policy/Schemas/signals-schema@1.json new file mode 100644 index 000000000..fc6829d8d --- /dev/null +++ b/src/Policy/__Libraries/StellaOps.Policy/Schemas/signals-schema@1.json @@ -0,0 +1,159 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.stellaops.io/policy/signals-schema@1.json", + "title": "StellaOps Policy Signals v1", + "description": "Defines available signals for policy condition evaluation", + "type": "object", + "$defs": { + "signalName": { + "type": "string", + "description": "A signal name for policy condition evaluation", + "pattern": "^[a-z][a-z0-9_]*(?:\\.[a-z][a-z0-9_]*)*$", + "examples": [ + "secret.has_finding", + "secret.severity.critical", + "secret.bundle.version", + "reachability.state", + "finding.severity" + ] + }, + "secretSignals": { + "type": "object", + "description": "Secret detection related signals", + "properties": { + "secret.has_finding": { + "type": "boolean", + "description": "True if any secret finding exists" + }, + "secret.count": { + "type": "integer", + "description": "Total number of secret findings", + "minimum": 0 + }, + "secret.severity.critical": { + "type": "boolean", + "description": "True if any critical severity secret finding exists" + }, + "secret.severity.high": { + "type": "boolean", + "description": "True if any high severity secret finding exists" + }, + "secret.severity.medium": { + "type": "boolean", + "description": "True if any medium severity secret finding exists" + }, + "secret.severity.low": { + "type": "boolean", + "description": "True if any low severity secret finding exists" + }, + "secret.confidence.high": { + "type": "boolean", + "description": "True if any high confidence secret finding exists" + }, + "secret.confidence.medium": { + "type": "boolean", + "description": "True if any medium confidence secret finding exists" + }, + "secret.confidence.low": { + "type": "boolean", + "description": "True if any low confidence secret finding exists" + }, + "secret.mask.applied": { + "type": "boolean", + "description": "True if masking was applied to all findings" + }, + "secret.bundle.version": { + "type": "string", + "description": "The active secret detection bundle version (YYYY.MM format)", + "pattern": "^\\d{4}\\.\\d{2}$" + }, + "secret.bundle.id": { + "type": "string", + "description": "The active bundle identifier" + }, + "secret.bundle.rule_count": { + "type": "integer", + "description": "Number of rules in the active bundle", + "minimum": 0 + }, + "secret.bundle.signer_key_id": { + "type": "string", + "description": "Key ID used to sign the bundle" + }, + "secret.aws.count": { + "type": "integer", + "description": "Count of AWS-related secret findings", + "minimum": 0 + }, + "secret.github.count": { + "type": "integer", + "description": "Count of GitHub-related secret findings", + "minimum": 0 + }, + "secret.private_key.count": { + "type": "integer", + "description": "Count of private key findings", + "minimum": 0 + } + } + }, + "findingSignals": { + "type": "object", + "description": "Vulnerability finding related signals", + "properties": { + "finding.severity": { + "type": "string", + "description": "Finding severity level", + "enum": ["critical", "high", "medium", "low", "unknown"] + }, + "finding.confidence": { + "type": "number", + "description": "Finding confidence score", + "minimum": 0, + "maximum": 1 + }, + "finding.cve_id": { + "type": "string", + "description": "CVE identifier if applicable" + } + } + }, + "reachabilitySignals": { + "type": "object", + "description": "Reachability analysis signals", + "properties": { + "reachability.state": { + "type": "string", + "description": "Reachability state", + "enum": ["reachable", "unreachable", "unknown"] + }, + "reachability.confidence": { + "type": "number", + "description": "Reachability confidence score", + "minimum": 0, + "maximum": 1 + }, + "reachability.has_runtime_evidence": { + "type": "boolean", + "description": "True if runtime evidence exists for reachability" + } + } + }, + "trustSignals": { + "type": "object", + "description": "Trust and verification signals", + "properties": { + "trust_score": { + "type": "number", + "description": "Trust score", + "minimum": 0, + "maximum": 1 + }, + "trust_verified": { + "type": "boolean", + "description": "True if the source is verified" + } + } + } + } +} diff --git a/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-schema@1.json b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-schema@1.json index b57d5d2c7..deed2c11e 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-schema@1.json +++ b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-schema@1.json @@ -131,6 +131,7 @@ }, "conditions": { "type": "array", + "description": "Conditions evaluated against policy signals. See signals-schema@1.json for available signals.", "items": { "type": "object", "additionalProperties": false, @@ -138,7 +139,18 @@ "properties": { "field": { "type": "string", - "maxLength": 256 + "maxLength": 256, + "description": "Signal name to evaluate. Common signals: secret.has_finding, secret.severity.critical, secret.count, secret.bundle.version, reachability.state, finding.severity", + "examples": [ + "secret.has_finding", + "secret.severity.critical", + "secret.severity.high", + "secret.count", + "secret.mask.applied", + "secret.bundle.version", + "reachability.state", + "finding.severity" + ] }, "operator": { "type": "string", @@ -153,8 +165,11 @@ "nin", "contains", "startsWith", - "endsWith" - ] + "endsWith", + "matches", + "exists" + ], + "description": "Comparison operator. 'matches' uses glob patterns, 'exists' checks signal presence." }, "value": {} } diff --git a/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-block@1.json b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-block@1.json new file mode 100644 index 000000000..1d764eba5 --- /dev/null +++ b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-block@1.json @@ -0,0 +1,82 @@ +{ + "apiVersion": "spl.stellaops/v1", + "kind": "Policy", + "metadata": { + "name": "secret-leak-block", + "description": "Block deployments with critical or high severity secret findings", + "labels": { + "category": "security", + "domain": "secrets" + } + }, + "spec": { + "defaultEffect": "allow", + "statements": [ + { + "id": "block-critical-secrets", + "effect": "deny", + "description": "Block any critical severity secret findings", + "match": { + "resource": "*", + "actions": ["deploy", "release"], + "conditions": [ + { + "field": "secret.severity.critical", + "operator": "eq", + "value": true + } + ] + }, + "audit": { + "message": "Blocked: Critical severity secret leak detected", + "severity": "error" + } + }, + { + "id": "block-high-secrets-unmasked", + "effect": "deny", + "description": "Block high severity secrets that are not properly masked", + "match": { + "resource": "*", + "actions": ["deploy", "release"], + "conditions": [ + { + "field": "secret.severity.high", + "operator": "eq", + "value": true + }, + { + "field": "secret.mask.applied", + "operator": "eq", + "value": false + } + ] + }, + "audit": { + "message": "Blocked: High severity secret without masking", + "severity": "error" + } + }, + { + "id": "require-current-bundle", + "effect": "deny", + "description": "Block scans using outdated detection bundles", + "match": { + "resource": "*", + "actions": ["deploy"], + "conditions": [ + { + "field": "secret.bundle.version", + "operator": "lt", + "value": "2025.01" + } + ] + }, + "audit": { + "message": "Blocked: Secret detection bundle is outdated", + "severity": "warn" + } + } + ] + } +} diff --git a/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-warn@1.json b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-warn@1.json new file mode 100644 index 000000000..9df53d541 --- /dev/null +++ b/src/Policy/__Libraries/StellaOps.Policy/Schemas/spl-secret-warn@1.json @@ -0,0 +1,98 @@ +{ + "apiVersion": "spl.stellaops/v1", + "kind": "Policy", + "metadata": { + "name": "secret-leak-warn", + "description": "Warn on secret findings without blocking deployments", + "labels": { + "category": "security", + "domain": "secrets", + "mode": "advisory" + } + }, + "spec": { + "defaultEffect": "allow", + "statements": [ + { + "id": "warn-any-secrets", + "effect": "allow", + "description": "Log warning for any secret findings", + "match": { + "resource": "*", + "actions": ["scan", "deploy", "release"], + "conditions": [ + { + "field": "secret.has_finding", + "operator": "eq", + "value": true + } + ] + }, + "audit": { + "message": "Warning: Secret findings detected in scan", + "severity": "warn" + } + }, + { + "id": "warn-aws-credentials", + "effect": "allow", + "description": "Special warning for AWS credential exposure", + "match": { + "resource": "*", + "actions": ["scan", "deploy"], + "conditions": [ + { + "field": "secret.aws.count", + "operator": "gt", + "value": 0 + } + ] + }, + "audit": { + "message": "Warning: AWS credentials detected - consider rotating", + "severity": "warn" + } + }, + { + "id": "warn-github-tokens", + "effect": "allow", + "description": "Special warning for GitHub token exposure", + "match": { + "resource": "*", + "actions": ["scan", "deploy"], + "conditions": [ + { + "field": "secret.github.count", + "operator": "gt", + "value": 0 + } + ] + }, + "audit": { + "message": "Warning: GitHub tokens detected - consider rotating", + "severity": "warn" + } + }, + { + "id": "warn-private-keys", + "effect": "allow", + "description": "Special warning for private key exposure", + "match": { + "resource": "*", + "actions": ["scan", "deploy"], + "conditions": [ + { + "field": "secret.private_key.count", + "operator": "gt", + "value": 0 + } + ] + }, + "audit": { + "message": "Warning: Private keys detected - review exposure", + "severity": "warn" + } + } + ] + } +} diff --git a/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretSignalBinder.cs b/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretSignalBinder.cs new file mode 100644 index 000000000..cba41d248 --- /dev/null +++ b/src/Policy/__Libraries/StellaOps.Policy/Secrets/SecretSignalBinder.cs @@ -0,0 +1,228 @@ +// ----------------------------------------------------------------------------- +// SecretSignalBinder.cs +// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration) +// Tasks: PSD-003 through PSD-007 - Add secret.* predicates as signals +// ----------------------------------------------------------------------------- + +using System.Collections.Immutable; +using System.Globalization; + +namespace StellaOps.Policy.Secrets; + +/// +/// Binds secret evidence to policy evaluation signals. +/// This class converts secret findings and bundle metadata into signals that can be +/// evaluated by the PolicyDsl SignalContext. +/// +/// +/// Available signals after binding: +/// +/// secret.has_finding - true if any secret finding exists +/// secret.count - total number of findings +/// secret.severity.critical - true if any critical finding exists +/// secret.severity.high - true if any high severity finding exists +/// secret.severity.medium - true if any medium severity finding exists +/// secret.severity.low - true if any low severity finding exists +/// secret.confidence.high - true if any high confidence finding exists +/// secret.confidence.medium - true if any medium confidence finding exists +/// secret.confidence.low - true if any low confidence finding exists +/// secret.mask.applied - true if masking was applied to all findings +/// secret.bundle.version - the active bundle version string +/// secret.bundle.id - the active bundle ID +/// secret.bundle.rule_count - the number of rules in the bundle +/// +/// +/// +public static class SecretSignalBinder +{ + /// + /// Signal name prefix for all secret-related signals. + /// + public const string SignalPrefix = "secret"; + + /// + /// Binds secret evidence to a dictionary of signals. + /// + /// The secret evidence context. + /// A dictionary of signal names to values. + public static ImmutableDictionary BindToSignals(SecretEvidenceContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var signals = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); + + // Core finding signals + signals[$"{SignalPrefix}.has_finding"] = context.HasAnyFinding; + signals[$"{SignalPrefix}.count"] = context.FindingCount; + + // Severity signals + signals[$"{SignalPrefix}.severity.critical"] = context.HasFindingWithSeverity("critical"); + signals[$"{SignalPrefix}.severity.high"] = context.HasFindingWithSeverity("high"); + signals[$"{SignalPrefix}.severity.medium"] = context.HasFindingWithSeverity("medium"); + signals[$"{SignalPrefix}.severity.low"] = context.HasFindingWithSeverity("low"); + + // Confidence signals + signals[$"{SignalPrefix}.confidence.high"] = context.HasFindingWithConfidence("high"); + signals[$"{SignalPrefix}.confidence.medium"] = context.HasFindingWithConfidence("medium"); + signals[$"{SignalPrefix}.confidence.low"] = context.HasFindingWithConfidence("low"); + + // Masking signal + signals[$"{SignalPrefix}.mask.applied"] = context.MaskingApplied; + + // Bundle signals + var bundle = context.Bundle; + if (bundle is not null) + { + signals[$"{SignalPrefix}.bundle.version"] = bundle.Version; + signals[$"{SignalPrefix}.bundle.id"] = bundle.BundleId; + signals[$"{SignalPrefix}.bundle.rule_count"] = bundle.RuleCount; + signals[$"{SignalPrefix}.bundle.signer_key_id"] = bundle.SignerKeyId; + } + else + { + signals[$"{SignalPrefix}.bundle.version"] = null; + signals[$"{SignalPrefix}.bundle.id"] = null; + signals[$"{SignalPrefix}.bundle.rule_count"] = 0; + signals[$"{SignalPrefix}.bundle.signer_key_id"] = null; + } + + // Rule-specific counts (for common rule patterns) + signals[$"{SignalPrefix}.aws.count"] = context.GetMatchCount("stellaops.secrets.aws-*"); + signals[$"{SignalPrefix}.github.count"] = context.GetMatchCount("stellaops.secrets.github-*"); + signals[$"{SignalPrefix}.private_key.count"] = context.GetMatchCount("stellaops.secrets.private-key-*"); + + return signals.ToImmutable(); + } + + /// + /// Binds secret evidence to a nested object suitable for member access in policies. + /// This creates a hierarchical structure like: + /// secret.severity.high, secret.bundle.version, etc. + /// + /// The secret evidence context. + /// A nested dictionary structure. + public static ImmutableDictionary BindToNestedObject(SecretEvidenceContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var severity = new Dictionary(StringComparer.Ordinal) + { + ["critical"] = context.HasFindingWithSeverity("critical"), + ["high"] = context.HasFindingWithSeverity("high"), + ["medium"] = context.HasFindingWithSeverity("medium"), + ["low"] = context.HasFindingWithSeverity("low"), + }; + + var confidence = new Dictionary(StringComparer.Ordinal) + { + ["high"] = context.HasFindingWithConfidence("high"), + ["medium"] = context.HasFindingWithConfidence("medium"), + ["low"] = context.HasFindingWithConfidence("low"), + }; + + var mask = new Dictionary(StringComparer.Ordinal) + { + ["applied"] = context.MaskingApplied, + }; + + var bundle = context.Bundle; + var bundleDict = new Dictionary(StringComparer.Ordinal) + { + ["version"] = bundle?.Version, + ["id"] = bundle?.BundleId, + ["rule_count"] = bundle?.RuleCount ?? 0, + ["signer_key_id"] = bundle?.SignerKeyId, + }; + + var match = new Dictionary(StringComparer.Ordinal) + { + ["count"] = context.FindingCount, + ["aws_count"] = context.GetMatchCount("stellaops.secrets.aws-*"), + ["github_count"] = context.GetMatchCount("stellaops.secrets.github-*"), + ["private_key_count"] = context.GetMatchCount("stellaops.secrets.private-key-*"), + }; + + return new Dictionary(StringComparer.Ordinal) + { + ["has_finding"] = context.HasAnyFinding, + ["count"] = context.FindingCount, + ["severity"] = severity, + ["confidence"] = confidence, + ["mask"] = mask, + ["bundle"] = bundleDict, + ["match"] = match, + }.ToImmutableDictionary(); + } + + /// + /// Checks if the bundle version meets a required minimum version. + /// + /// The secret evidence context. + /// The minimum required version (YYYY.MM format). + /// True if the bundle meets the version requirement. + public static bool CheckBundleVersion(SecretEvidenceContext context, string requiredVersion) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(requiredVersion); + + return context.BundleVersionMeetsRequirement(requiredVersion); + } + + /// + /// Checks if all findings are in paths matching the allowlist. + /// + /// The secret evidence context. + /// Glob patterns for allowed paths. + /// True if all findings are in allowed paths. + public static bool CheckPathAllowlist(SecretEvidenceContext context, IReadOnlyList patterns) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(patterns); + + return context.AllFindingsInAllowlist(patterns); + } + + /// + /// Creates finding summary for policy explanation. + /// + /// The secret evidence context. + /// A summary string for audit/explanation purposes. + public static string CreateFindingSummary(SecretEvidenceContext context) + { + ArgumentNullException.ThrowIfNull(context); + + if (!context.HasAnyFinding) + { + return "No secret findings detected."; + } + + var findings = context.Findings; + var severityCounts = findings + .GroupBy(f => f.Severity, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + var parts = new List(); + if (severityCounts.TryGetValue("critical", out var critical) && critical > 0) + { + parts.Add($"{critical} critical"); + } + if (severityCounts.TryGetValue("high", out var high) && high > 0) + { + parts.Add($"{high} high"); + } + if (severityCounts.TryGetValue("medium", out var medium) && medium > 0) + { + parts.Add($"{medium} medium"); + } + if (severityCounts.TryGetValue("low", out var low) && low > 0) + { + parts.Add($"{low} low"); + } + + return string.Format( + CultureInfo.InvariantCulture, + "{0} secret finding(s): {1}", + findings.Count, + string.Join(", ", parts)); + } +} diff --git a/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj b/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj index 04d8ab1a8..0f420b161 100644 --- a/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj +++ b/src/Policy/__Libraries/StellaOps.Policy/StellaOps.Policy.csproj @@ -19,8 +19,11 @@ + + + diff --git a/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretEvidenceContextTests.cs b/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretEvidenceContextTests.cs new file mode 100644 index 000000000..fcdeb84c4 --- /dev/null +++ b/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretEvidenceContextTests.cs @@ -0,0 +1,259 @@ +// ----------------------------------------------------------------------------- +// SecretEvidenceContextTests.cs +// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration) +// Task: PSD-011 - Add unit and integration tests +// ----------------------------------------------------------------------------- + +using System.Collections.Immutable; +using FluentAssertions; +using Moq; +using StellaOps.Policy.Secrets; +using Xunit; + +namespace StellaOps.Policy.Tests.Secrets; + +[Trait("Category", "Unit")] +public sealed class SecretEvidenceContextTests +{ + [Fact] + public void Constructor_WithNullProvider_ThrowsArgumentNullException() + { + var action = () => new SecretEvidenceContext(null!); + action.Should().Throw(); + } + + [Fact] + public void HasAnyFinding_NoFindings_ReturnsFalse() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + context.HasAnyFinding.Should().BeFalse(); + } + + [Fact] + public void HasAnyFinding_WithFindings_ReturnsTrue() + { + var finding = CreateFinding("stellaops.secrets.test", "high"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasAnyFinding.Should().BeTrue(); + } + + [Fact] + public void FindingCount_ReturnsCorrectCount() + { + var findings = new[] + { + CreateFinding("stellaops.secrets.test1", "high"), + CreateFinding("stellaops.secrets.test2", "medium"), + CreateFinding("stellaops.secrets.test3", "low"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + context.FindingCount.Should().Be(3); + } + + [Fact] + public void HasFindingWithSeverity_MatchingSeverity_ReturnsTrue() + { + var finding = CreateFinding("stellaops.secrets.test", "critical"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithSeverity("critical").Should().BeTrue(); + context.HasFindingWithSeverity("CRITICAL").Should().BeTrue(); // Case insensitive + } + + [Fact] + public void HasFindingWithSeverity_NoMatchingSeverity_ReturnsFalse() + { + var finding = CreateFinding("stellaops.secrets.test", "low"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithSeverity("critical").Should().BeFalse(); + } + + [Fact] + public void HasFindingWithConfidence_MatchingConfidence_ReturnsTrue() + { + var finding = CreateFinding("stellaops.secrets.test", "high", "high"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithConfidence("high").Should().BeTrue(); + } + + [Fact] + public void HasFindingWithRuleId_ExactMatch_ReturnsTrue() + { + var finding = CreateFinding("stellaops.secrets.aws-access-key", "high"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithRuleId("stellaops.secrets.aws-access-key").Should().BeTrue(); + } + + [Fact] + public void HasFindingWithRuleId_PatternMatch_ReturnsTrue() + { + var finding = CreateFinding("stellaops.secrets.aws-access-key", "high"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithRuleId("stellaops.secrets.aws-*").Should().BeTrue(); + } + + [Fact] + public void HasFindingWithRuleId_NoMatch_ReturnsFalse() + { + var finding = CreateFinding("stellaops.secrets.github-token", "high"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + context.HasFindingWithRuleId("stellaops.secrets.aws-*").Should().BeFalse(); + } + + [Fact] + public void GetMatchCount_WithPattern_ReturnsCorrectCount() + { + var findings = new[] + { + CreateFinding("stellaops.secrets.aws-access-key", "high"), + CreateFinding("stellaops.secrets.aws-secret-key", "high"), + CreateFinding("stellaops.secrets.github-token", "medium"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + context.GetMatchCount("stellaops.secrets.aws-*").Should().Be(2); + context.GetMatchCount("stellaops.secrets.github-*").Should().Be(1); + } + + [Fact] + public void BundleVersionMeetsRequirement_ValidVersion_ReturnsTrue() + { + var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([], bundle); + var context = new SecretEvidenceContext(provider); + + context.BundleVersionMeetsRequirement("2025.01").Should().BeTrue(); + context.BundleVersionMeetsRequirement("2025.06").Should().BeTrue(); + } + + [Fact] + public void BundleVersionMeetsRequirement_OlderVersion_ReturnsFalse() + { + var bundle = new SecretBundleMetadata("bundle-1", "2024.12", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([], bundle); + var context = new SecretEvidenceContext(provider); + + context.BundleVersionMeetsRequirement("2025.01").Should().BeFalse(); + } + + [Fact] + public void BundleVersionMeetsRequirement_NoBundle_ReturnsFalse() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + context.BundleVersionMeetsRequirement("2025.01").Should().BeFalse(); + } + + [Fact] + public void MaskingApplied_ReturnsProviderValue() + { + var mock = new Mock(); + mock.Setup(p => p.GetFindings()).Returns([]); + mock.Setup(p => p.IsMaskingApplied()).Returns(true); + var context = new SecretEvidenceContext(mock.Object); + + context.MaskingApplied.Should().BeTrue(); + } + + [Fact] + public void AllFindingsInAllowlist_NoFindings_ReturnsTrue() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + context.AllFindingsInAllowlist(["**/test/**"]).Should().BeTrue(); + } + + [Fact] + public void AllFindingsInAllowlist_AllMatch_ReturnsTrue() + { + var findings = new[] + { + CreateFinding("rule1", "high", "high", "test/data/secrets.txt"), + CreateFinding("rule2", "high", "high", "test/fixtures/keys.txt"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + context.AllFindingsInAllowlist(["test/**"]).Should().BeTrue(); + } + + [Fact] + public void AllFindingsInAllowlist_SomeNotMatch_ReturnsFalse() + { + var findings = new[] + { + CreateFinding("rule1", "high", "high", "test/data/secrets.txt"), + CreateFinding("rule2", "high", "high", "src/app/config.json"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + context.AllFindingsInAllowlist(["test/**"]).Should().BeFalse(); + } + + [Fact] + public void AllFindingsInAllowlist_DoubleStarPattern_MatchesNestedPaths() + { + var findings = new[] + { + CreateFinding("rule1", "high", "high", "a/b/c/d/test.txt"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + context.AllFindingsInAllowlist(["**/test.txt"]).Should().BeTrue(); + } + + private static ISecretEvidenceProvider CreateMockProvider( + SecretFinding[] findings, + SecretBundleMetadata? bundle = null, + bool maskingApplied = true) + { + var mock = new Mock(); + mock.Setup(p => p.GetFindings()).Returns(findings); + mock.Setup(p => p.GetBundleMetadata()).Returns(bundle); + mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied); + return mock.Object; + } + + private static SecretFinding CreateFinding( + string ruleId, + string severity, + string confidence = "high", + string filePath = "test/file.txt") + { + return new SecretFinding + { + RuleId = ruleId, + RuleVersion = "1.0.0", + Severity = severity, + Confidence = confidence, + FilePath = filePath, + LineNumber = 10, + Mask = "***REDACTED***", + BundleId = "bundle-1", + BundleVersion = "2025.01", + DetectedAt = DateTimeOffset.UtcNow, + }; + } +} diff --git a/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretSignalBinderTests.cs b/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretSignalBinderTests.cs new file mode 100644 index 000000000..4867fa662 --- /dev/null +++ b/src/Policy/__Tests/StellaOps.Policy.Tests/Secrets/SecretSignalBinderTests.cs @@ -0,0 +1,264 @@ +// ----------------------------------------------------------------------------- +// SecretSignalBinderTests.cs +// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration) +// Task: PSD-011 - Add unit and integration tests +// ----------------------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using StellaOps.Policy.Secrets; +using Xunit; + +namespace StellaOps.Policy.Tests.Secrets; + +[Trait("Category", "Unit")] +public sealed class SecretSignalBinderTests +{ + [Fact] + public void BindToSignals_WithNullContext_ThrowsArgumentNullException() + { + var action = () => SecretSignalBinder.BindToSignals(null!); + action.Should().Throw(); + } + + [Fact] + public void BindToSignals_NoFindings_ReturnsExpectedSignals() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.has_finding"].Should().Be(false); + signals["secret.count"].Should().Be(0); + signals["secret.severity.critical"].Should().Be(false); + signals["secret.severity.high"].Should().Be(false); + signals["secret.severity.medium"].Should().Be(false); + signals["secret.severity.low"].Should().Be(false); + } + + [Fact] + public void BindToSignals_WithCriticalFinding_SetsCriticalSignal() + { + var finding = CreateFinding("rule1", "critical"); + var provider = CreateMockProvider([finding]); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.has_finding"].Should().Be(true); + signals["secret.count"].Should().Be(1); + signals["secret.severity.critical"].Should().Be(true); + } + + [Fact] + public void BindToSignals_WithMultipleSeverities_SetsAllMatchingSignals() + { + var findings = new[] + { + CreateFinding("rule1", "high"), + CreateFinding("rule2", "medium"), + CreateFinding("rule3", "high"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.has_finding"].Should().Be(true); + signals["secret.count"].Should().Be(3); + signals["secret.severity.critical"].Should().Be(false); + signals["secret.severity.high"].Should().Be(true); + signals["secret.severity.medium"].Should().Be(true); + signals["secret.severity.low"].Should().Be(false); + } + + [Fact] + public void BindToSignals_WithBundle_SetsBundleSignals() + { + var bundle = new SecretBundleMetadata( + "stellaops-bundle-2025", + "2025.06", + DateTimeOffset.UtcNow, + 150, + "key-001"); + var provider = CreateMockProvider([], bundle); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.bundle.version"].Should().Be("2025.06"); + signals["secret.bundle.id"].Should().Be("stellaops-bundle-2025"); + signals["secret.bundle.rule_count"].Should().Be(150); + signals["secret.bundle.signer_key_id"].Should().Be("key-001"); + } + + [Fact] + public void BindToSignals_NoBundle_SetsBundleSignalsToDefaults() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.bundle.version"].Should().BeNull(); + signals["secret.bundle.id"].Should().BeNull(); + signals["secret.bundle.rule_count"].Should().Be(0); + signals["secret.bundle.signer_key_id"].Should().BeNull(); + } + + [Fact] + public void BindToSignals_WithConfidenceLevels_SetsConfidenceSignals() + { + var findings = new[] + { + CreateFinding("rule1", "high", "high"), + CreateFinding("rule2", "high", "medium"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.confidence.high"].Should().Be(true); + signals["secret.confidence.medium"].Should().Be(true); + signals["secret.confidence.low"].Should().Be(false); + } + + [Fact] + public void BindToSignals_WithMasking_SetsMaskSignal() + { + var provider = CreateMockProvider([], maskingApplied: true); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.mask.applied"].Should().Be(true); + } + + [Fact] + public void BindToSignals_SetsRuleSpecificCounts() + { + var findings = new[] + { + CreateFinding("stellaops.secrets.aws-access-key", "high"), + CreateFinding("stellaops.secrets.aws-secret-key", "high"), + CreateFinding("stellaops.secrets.github-token", "medium"), + CreateFinding("stellaops.secrets.private-key-rsa", "critical"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + var signals = SecretSignalBinder.BindToSignals(context); + + signals["secret.aws.count"].Should().Be(2); + signals["secret.github.count"].Should().Be(1); + signals["secret.private_key.count"].Should().Be(1); + } + + [Fact] + public void BindToNestedObject_ReturnsHierarchicalStructure() + { + var finding = CreateFinding("rule1", "critical", "high"); + var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([finding], bundle); + var context = new SecretEvidenceContext(provider); + + var nested = SecretSignalBinder.BindToNestedObject(context); + + nested["has_finding"].Should().Be(true); + nested["count"].Should().Be(1); + + var severity = nested["severity"] as IDictionary; + severity.Should().NotBeNull(); + severity!["critical"].Should().Be(true); + + var bundleDict = nested["bundle"] as IDictionary; + bundleDict.Should().NotBeNull(); + bundleDict!["version"].Should().Be("2025.06"); + } + + [Fact] + public void CheckBundleVersion_ValidRequirement_ReturnsTrue() + { + var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([], bundle); + var context = new SecretEvidenceContext(provider); + + SecretSignalBinder.CheckBundleVersion(context, "2025.01").Should().BeTrue(); + } + + [Fact] + public void CheckBundleVersion_InvalidRequirement_ReturnsFalse() + { + var bundle = new SecretBundleMetadata("bundle-1", "2024.12", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([], bundle); + var context = new SecretEvidenceContext(provider); + + SecretSignalBinder.CheckBundleVersion(context, "2025.01").Should().BeFalse(); + } + + [Fact] + public void CreateFindingSummary_NoFindings_ReturnsNoFindingsMessage() + { + var provider = CreateMockProvider([]); + var context = new SecretEvidenceContext(provider); + + var summary = SecretSignalBinder.CreateFindingSummary(context); + + summary.Should().Be("No secret findings detected."); + } + + [Fact] + public void CreateFindingSummary_WithFindings_ReturnsFormattedSummary() + { + var findings = new[] + { + CreateFinding("rule1", "critical"), + CreateFinding("rule2", "high"), + CreateFinding("rule3", "high"), + CreateFinding("rule4", "medium"), + }; + var provider = CreateMockProvider(findings); + var context = new SecretEvidenceContext(provider); + + var summary = SecretSignalBinder.CreateFindingSummary(context); + + summary.Should().Contain("4 secret finding(s)"); + summary.Should().Contain("1 critical"); + summary.Should().Contain("2 high"); + summary.Should().Contain("1 medium"); + } + + private static ISecretEvidenceProvider CreateMockProvider( + SecretFinding[] findings, + SecretBundleMetadata? bundle = null, + bool maskingApplied = true) + { + var mock = new Mock(); + mock.Setup(p => p.GetFindings()).Returns(findings); + mock.Setup(p => p.GetBundleMetadata()).Returns(bundle); + mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied); + return mock.Object; + } + + private static SecretFinding CreateFinding( + string ruleId, + string severity, + string confidence = "high") + { + return new SecretFinding + { + RuleId = ruleId, + RuleVersion = "1.0.0", + Severity = severity, + Confidence = confidence, + FilePath = "test/file.txt", + LineNumber = 10, + Mask = "***REDACTED***", + BundleId = "bundle-1", + BundleVersion = "2025.01", + DetectedAt = DateTimeOffset.UtcNow, + }; + } +} diff --git a/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/SecretSignalContextExtensionsTests.cs b/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/SecretSignalContextExtensionsTests.cs new file mode 100644 index 000000000..d73638da1 --- /dev/null +++ b/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/SecretSignalContextExtensionsTests.cs @@ -0,0 +1,182 @@ +// ----------------------------------------------------------------------------- +// SecretSignalContextExtensionsTests.cs +// Sprint: SPRINT_20260104_004_POLICY (Secret DSL Integration) +// Task: PSD-011 - Add unit and integration tests +// ----------------------------------------------------------------------------- + +using FluentAssertions; +using Moq; +using StellaOps.Policy.Secrets; +using Xunit; + +namespace StellaOps.PolicyDsl.Tests; + +[Trait("Category", "Unit")] +public sealed class SecretSignalContextExtensionsTests +{ + [Fact] + public void WithSecretEvidence_OnSignalContext_AddsAllSignals() + { + var finding = CreateFinding("stellaops.secrets.aws-key", "critical", "high"); + var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 100); + var provider = CreateMockProvider([finding], bundle, true); + var evidenceContext = new SecretEvidenceContext(provider); + + var signalContext = new SignalContext(); + signalContext.WithSecretEvidence(evidenceContext); + + // Check flat signals + signalContext.GetSignal("secret.has_finding").Should().BeTrue(); + signalContext.GetSignal("secret.count").Should().Be(1); + signalContext.GetSignal("secret.severity.critical").Should().BeTrue(); + signalContext.GetSignal("secret.mask.applied").Should().BeTrue(); + signalContext.GetSignal("secret.bundle.version").Should().Be("2025.06"); + + // Check nested object + var secretObj = signalContext.GetSignal("secret") as IDictionary; + secretObj.Should().NotBeNull(); + secretObj!["has_finding"].Should().Be(true); + } + + [Fact] + public void WithSecretEvidence_OnSignalContextBuilder_AddsAllSignals() + { + var finding = CreateFinding("stellaops.secrets.github-token", "high", "medium"); + var provider = CreateMockProvider([finding]); + var evidenceContext = new SecretEvidenceContext(provider); + + var context = SignalContext.Builder() + .WithSecretEvidence(evidenceContext) + .Build(); + + context.GetSignal("secret.has_finding").Should().BeTrue(); + context.GetSignal("secret.severity.high").Should().BeTrue(); + context.GetSignal("secret.confidence.medium").Should().BeTrue(); + } + + [Fact] + public void WithSecretEvidence_FromProvider_AddsSignals() + { + var finding = CreateFinding("stellaops.secrets.private-key-rsa", "critical"); + var provider = CreateMockProvider([finding]); + + var context = SignalContext.Builder() + .WithSecretEvidence(provider) + .Build(); + + context.GetSignal("secret.has_finding").Should().BeTrue(); + context.GetSignal("secret.private_key.count").Should().Be(1); + } + + [Fact] + public void CreateBuilderWithSecrets_ReturnsConfiguredBuilder() + { + var provider = CreateMockProvider([]); + var evidenceContext = new SecretEvidenceContext(provider); + + var builder = SecretSignalContextExtensions.CreateBuilderWithSecrets(evidenceContext); + var context = builder.Build(); + + context.GetSignal("secret.has_finding").Should().BeFalse(); + context.GetSignal("secret.count").Should().Be(0); + } + + [Fact] + public void CreateContextWithSecrets_ReturnsFullyConfiguredContext() + { + var findings = new[] + { + CreateFinding("rule1", "high"), + CreateFinding("rule2", "medium"), + }; + var bundle = new SecretBundleMetadata("bundle-1", "2025.06", DateTimeOffset.UtcNow, 50); + var provider = CreateMockProvider(findings, bundle); + var evidenceContext = new SecretEvidenceContext(provider); + + var context = SecretSignalContextExtensions.CreateContextWithSecrets(evidenceContext); + + context.GetSignal("secret.has_finding").Should().BeTrue(); + context.GetSignal("secret.count").Should().Be(2); + context.GetSignal("secret.bundle.id").Should().Be("bundle-1"); + } + + [Fact] + public void WithSecretEvidence_NullContext_ThrowsArgumentNullException() + { + SignalContext context = null!; + var provider = CreateMockProvider([]); + var evidenceContext = new SecretEvidenceContext(provider); + + var action = () => context.WithSecretEvidence(evidenceContext); + action.Should().Throw(); + } + + [Fact] + public void WithSecretEvidence_NullEvidenceContext_ThrowsArgumentNullException() + { + var context = new SignalContext(); + SecretEvidenceContext evidenceContext = null!; + + var action = () => context.WithSecretEvidence(evidenceContext); + action.Should().Throw(); + } + + [Fact] + public void SignalContext_CanCombineSecretSignalsWithOtherSignals() + { + var finding = CreateFinding("rule1", "high"); + var provider = CreateMockProvider([finding]); + var evidenceContext = new SecretEvidenceContext(provider); + + var context = SignalContext.Builder() + .WithFlag("custom.flag", true) + .WithNumber("custom.score", 0.85m) + .WithSecretEvidence(evidenceContext) + .WithFinding("high", 0.9m, "CVE-2025-1234") + .Build(); + + // Custom signals preserved + context.GetSignal("custom.flag").Should().BeTrue(); + context.GetSignal("custom.score").Should().Be(0.85m); + + // Secret signals added + context.GetSignal("secret.has_finding").Should().BeTrue(); + + // Other builder methods work + var finding2 = context.GetSignal("finding") as IDictionary; + finding2.Should().NotBeNull(); + finding2!["severity"].Should().Be("high"); + } + + private static ISecretEvidenceProvider CreateMockProvider( + SecretFinding[] findings, + SecretBundleMetadata? bundle = null, + bool maskingApplied = true) + { + var mock = new Mock(); + mock.Setup(p => p.GetFindings()).Returns(findings); + mock.Setup(p => p.GetBundleMetadata()).Returns(bundle); + mock.Setup(p => p.IsMaskingApplied()).Returns(maskingApplied); + return mock.Object; + } + + private static SecretFinding CreateFinding( + string ruleId, + string severity, + string confidence = "high") + { + return new SecretFinding + { + RuleId = ruleId, + RuleVersion = "1.0.0", + Severity = severity, + Confidence = confidence, + FilePath = "test/file.txt", + LineNumber = 10, + Mask = "***REDACTED***", + BundleId = "bundle-1", + BundleVersion = "2025.01", + DetectedAt = DateTimeOffset.UtcNow, + }; + } +} diff --git a/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/StellaOps.PolicyDsl.Tests.csproj b/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/StellaOps.PolicyDsl.Tests.csproj index 85b48494e..2ea1705d3 100644 --- a/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/StellaOps.PolicyDsl.Tests.csproj +++ b/src/Policy/__Tests/StellaOps.PolicyDsl.Tests/StellaOps.PolicyDsl.Tests.csproj @@ -11,9 +11,10 @@ - + + diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/InMemoryWebhookRateLimiter.cs b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/InMemoryWebhookRateLimiter.cs index 4643ccbcb..fc1249771 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/InMemoryWebhookRateLimiter.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/EventWebhooks/InMemoryWebhookRateLimiter.cs @@ -7,9 +7,15 @@ namespace StellaOps.Scheduler.WebService.EventWebhooks; internal sealed class InMemoryWebhookRateLimiter : IWebhookRateLimiter, IDisposable { private readonly MemoryCache _cache = new(new MemoryCacheOptions()); + private readonly TimeProvider _timeProvider; private readonly object _mutex = new(); + public InMemoryWebhookRateLimiter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + public bool TryAcquire(string key, int limit, TimeSpan window, out TimeSpan retryAfter) { if (limit <= 0) @@ -19,7 +25,7 @@ internal sealed class InMemoryWebhookRateLimiter : IWebhookRateLimiter, IDisposa } retryAfter = TimeSpan.Zero; - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); lock (_mutex) { diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobService.cs b/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobService.cs index 4a3059dd1..74ec79748 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobService.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/GraphJobs/GraphJobService.cs @@ -8,18 +8,18 @@ namespace StellaOps.Scheduler.WebService.GraphJobs; internal sealed class GraphJobService : IGraphJobService { private readonly IGraphJobStore _store; - private readonly ISystemClock _clock; + private readonly TimeProvider _timeProvider; private readonly IGraphJobCompletionPublisher _completionPublisher; private readonly ICartographerWebhookClient _cartographerWebhook; public GraphJobService( IGraphJobStore store, - ISystemClock clock, + TimeProvider timeProvider, IGraphJobCompletionPublisher completionPublisher, ICartographerWebhookClient cartographerWebhook) { _store = store ?? throw new ArgumentNullException(nameof(store)); - _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); _completionPublisher = completionPublisher ?? throw new ArgumentNullException(nameof(completionPublisher)); _cartographerWebhook = cartographerWebhook ?? throw new ArgumentNullException(nameof(cartographerWebhook)); } @@ -31,7 +31,7 @@ internal sealed class GraphJobService : IGraphJobService var trigger = request.Trigger ?? GraphBuildJobTrigger.SbomVersion; var metadata = request.Metadata ?? new Dictionary(StringComparer.Ordinal); - var now = _clock.UtcNow; + var now = _timeProvider.GetUtcNow(); var id = GenerateIdentifier("gbj"); var job = new GraphBuildJob( id, @@ -65,7 +65,7 @@ internal sealed class GraphJobService : IGraphJobService var metadata = request.Metadata ?? new Dictionary(StringComparer.Ordinal); var trigger = request.Trigger ?? GraphOverlayJobTrigger.Policy; - var now = _clock.UtcNow; + var now = _timeProvider.GetUtcNow(); var id = GenerateIdentifier("goj"); var job = new GraphOverlayJob( @@ -98,7 +98,7 @@ internal sealed class GraphJobService : IGraphJobService throw new ValidationException("Completion requires status completed, failed, or cancelled."); } - var occurredAt = request.OccurredAt == default ? _clock.UtcNow : request.OccurredAt.ToUniversalTime(); + var occurredAt = request.OccurredAt == default ? _timeProvider.GetUtcNow() : request.OccurredAt.ToUniversalTime(); var graphSnapshotId = Normalize(request.GraphSnapshotId); var correlationId = Normalize(request.CorrelationId); var resultUri = Normalize(request.ResultUri); @@ -369,7 +369,7 @@ internal sealed class GraphJobService : IGraphJobService public async Task GetOverlayLagMetricsAsync(string tenantId, CancellationToken cancellationToken) { - var now = _clock.UtcNow; + var now = _timeProvider.GetUtcNow(); var overlayJobs = await _store.GetOverlayJobsAsync(tenantId, cancellationToken); var pending = overlayJobs.Count(job => job.Status == GraphJobStatus.Pending); diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/ISystemClock.cs b/src/Scheduler/StellaOps.Scheduler.WebService/ISystemClock.cs index 76adff77e..3a979be5a 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/ISystemClock.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/ISystemClock.cs @@ -1,11 +1,26 @@ namespace StellaOps.Scheduler.WebService; +/// +/// Legacy system clock interface. Prefer using TimeProvider instead. +/// +[Obsolete("Use TimeProvider instead. This interface is retained for backward compatibility.")] public interface ISystemClock { DateTimeOffset UtcNow { get; } } +/// +/// Legacy system clock implementation. Prefer using TimeProvider instead. +/// +[Obsolete("Use TimeProvider instead. This class is retained for backward compatibility.")] public sealed class SystemClock : ISystemClock { - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; + private readonly TimeProvider _timeProvider; + + public SystemClock(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + + public DateTimeOffset UtcNow => _timeProvider.GetUtcNow(); } diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/InMemoryPolicyRunService.cs b/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/InMemoryPolicyRunService.cs index ec8909460..1a382a112 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/InMemoryPolicyRunService.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/PolicyRuns/InMemoryPolicyRunService.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; +using StellaOps.Determinism; using StellaOps.Scheduler.Models; namespace StellaOps.Scheduler.WebService.PolicyRuns; @@ -10,6 +11,14 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService private readonly ConcurrentDictionary _runs = new(StringComparer.Ordinal); private readonly List _orderedRuns = new(); private readonly object _gate = new(); + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; + + public InMemoryPolicyRunService(TimeProvider? timeProvider = null, IGuidProvider? guidProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; + } public Task EnqueueAsync(string tenantId, PolicyRunRequest request, CancellationToken cancellationToken) { @@ -17,11 +26,12 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService ArgumentNullException.ThrowIfNull(request); cancellationToken.ThrowIfCancellationRequested(); + var now = _timeProvider.GetUtcNow(); var runId = string.IsNullOrWhiteSpace(request.RunId) - ? GenerateRunId(request.PolicyId, request.QueuedAt ?? DateTimeOffset.UtcNow) + ? GenerateRunId(request.PolicyId, request.QueuedAt ?? now) : request.RunId; - var queuedAt = request.QueuedAt ?? DateTimeOffset.UtcNow; + var queuedAt = request.QueuedAt ?? now; var status = new PolicyRunStatus( runId, @@ -152,7 +162,7 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService } var cancellationReason = NormalizeCancellationReason(reason); - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); updated = existing with { Status = PolicyRunExecutionStatus.Cancelled, @@ -206,17 +216,17 @@ internal sealed class InMemoryPolicyRunService : IPolicyRunService runId: null, policyVersion: existing.PolicyVersion, requestedBy: NormalizeActor(requestedBy), - queuedAt: DateTimeOffset.UtcNow, + queuedAt: _timeProvider.GetUtcNow(), correlationId: null, metadata: metadataBuilder.ToImmutable()); return await EnqueueAsync(tenantId, request, cancellationToken).ConfigureAwait(false); } - private static string GenerateRunId(string policyId, DateTimeOffset timestamp) + private string GenerateRunId(string policyId, DateTimeOffset timestamp) { var normalizedPolicyId = string.IsNullOrWhiteSpace(policyId) ? "policy" : policyId.Trim(); - var suffix = Guid.NewGuid().ToString("N")[..8]; + var suffix = _guidProvider.NewGuid().ToString("N")[..8]; return $"run:{normalizedPolicyId}:{timestamp:yyyyMMddTHHmmssZ}:{suffix}"; } diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs index df131f406..ec8996d4d 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Program.cs @@ -29,7 +29,7 @@ using StellaOps.Router.AspNet; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRouting(options => options.LowercaseUrls = true); -builder.Services.AddSingleton(); +// TimeProvider.System is registered here for deterministic time support builder.Services.TryAddSingleton(TimeProvider.System); var authorityOptions = new SchedulerAuthorityOptions(); diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/SchedulerEndpointHelpers.cs b/src/Scheduler/StellaOps.Scheduler.WebService/SchedulerEndpointHelpers.cs index f1cc8de5d..781239a7c 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/SchedulerEndpointHelpers.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/SchedulerEndpointHelpers.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Text; +using StellaOps.Determinism; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.Persistence.Postgres.Repositories; @@ -13,14 +14,15 @@ internal static class SchedulerEndpointHelpers private const string ActorKindHeader = "X-Actor-Kind"; private const string TenantHeader = "X-Tenant-Id"; - public static string GenerateIdentifier(string prefix) + public static string GenerateIdentifier(string prefix, IGuidProvider? guidProvider = null) { if (string.IsNullOrWhiteSpace(prefix)) { throw new ArgumentException("Prefix must be provided.", nameof(prefix)); } - return $"{prefix.Trim()}_{Guid.NewGuid():N}"; + var guid = (guidProvider ?? SystemGuidProvider.Instance).NewGuid(); + return $"{prefix.Trim()}_{guid:N}"; } public static string ResolveActorId(HttpContext context) diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/InMemorySchedulerServices.cs b/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/InMemorySchedulerServices.cs index 9f321b6a9..e545d57b7 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/InMemorySchedulerServices.cs +++ b/src/Scheduler/StellaOps.Scheduler.WebService/Schedules/InMemorySchedulerServices.cs @@ -124,11 +124,22 @@ internal sealed class InMemoryRunSummaryService : IRunSummaryService internal sealed class InMemorySchedulerAuditService : ISchedulerAuditService { + private readonly TimeProvider _timeProvider; + private readonly StellaOps.Determinism.IGuidProvider _guidProvider; + + public InMemorySchedulerAuditService( + TimeProvider? timeProvider = null, + StellaOps.Determinism.IGuidProvider? guidProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; + } + public Task WriteAsync(SchedulerAuditEvent auditEvent, CancellationToken cancellationToken = default) { - var occurredAt = auditEvent.OccurredAt ?? DateTimeOffset.UtcNow; + var occurredAt = auditEvent.OccurredAt ?? _timeProvider.GetUtcNow(); var record = new AuditRecord( - auditEvent.AuditId ?? $"audit_{Guid.NewGuid():N}", + auditEvent.AuditId ?? $"audit_{_guidProvider.NewGuid():N}", auditEvent.TenantId, auditEvent.Category, auditEvent.Action, diff --git a/src/Scheduler/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj b/src/Scheduler/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj index 3f28f7199..effde0a12 100644 --- a/src/Scheduler/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj +++ b/src/Scheduler/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/FailureSignatureRepository.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/FailureSignatureRepository.cs index 09db8f84c..cf2abc12a 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/FailureSignatureRepository.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/FailureSignatureRepository.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Npgsql; +using StellaOps.Determinism; using StellaOps.Infrastructure.Postgres.Repositories; using StellaOps.Scheduler.Persistence.Postgres.Models; @@ -10,12 +11,21 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories; /// public sealed class FailureSignatureRepository : RepositoryBase, IFailureSignatureRepository { + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; + /// /// Creates a new failure signature repository. /// - public FailureSignatureRepository(SchedulerDataSource dataSource, ILogger logger) + public FailureSignatureRepository( + SchedulerDataSource dataSource, + ILogger logger, + TimeProvider? timeProvider = null, + IGuidProvider? guidProvider = null) : base(dataSource, logger) { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } /// @@ -332,7 +342,7 @@ public sealed class FailureSignatureRepository : RepositoryBase public sealed class JobRepository : RepositoryBase, IJobRepository { + private readonly TimeProvider _timeProvider; + private readonly IGuidProvider _guidProvider; + /// /// Creates a new job repository. /// - public JobRepository(SchedulerDataSource dataSource, ILogger logger) + public JobRepository( + SchedulerDataSource dataSource, + ILogger logger, + TimeProvider? timeProvider = null, + IGuidProvider? guidProvider = null) : base(dataSource, logger) { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } /// @@ -123,8 +133,8 @@ public sealed class JobRepository : RepositoryBase, IJobRep TimeSpan leaseDuration, CancellationToken cancellationToken = default) { - var leaseId = Guid.NewGuid(); - var leaseUntil = DateTimeOffset.UtcNow.Add(leaseDuration); + var leaseId = _guidProvider.NewGuid(); + var leaseUntil = _timeProvider.GetUtcNow().Add(leaseDuration); const string sql = """ UPDATE scheduler.jobs diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/TriggerRepository.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/TriggerRepository.cs index f5562b348..e5d652385 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/TriggerRepository.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/Postgres/Repositories/TriggerRepository.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Npgsql; +using StellaOps.Determinism; using StellaOps.Infrastructure.Postgres.Repositories; using StellaOps.Scheduler.Persistence.Postgres.Models; @@ -10,12 +11,18 @@ namespace StellaOps.Scheduler.Persistence.Postgres.Repositories; /// public sealed class TriggerRepository : RepositoryBase, ITriggerRepository { + private readonly IGuidProvider _guidProvider; + /// /// Creates a new trigger repository. /// - public TriggerRepository(SchedulerDataSource dataSource, ILogger logger) + public TriggerRepository( + SchedulerDataSource dataSource, + ILogger logger, + IGuidProvider? guidProvider = null) : base(dataSource, logger) { + _guidProvider = guidProvider ?? SystemGuidProvider.Instance; } /// @@ -125,7 +132,7 @@ public sealed class TriggerRepository : RepositoryBase, ITr RETURNING * """; - var id = trigger.Id == Guid.Empty ? Guid.NewGuid() : trigger.Id; + var id = trigger.Id == Guid.Empty ? _guidProvider.NewGuid() : trigger.Id; await using var connection = await DataSource.OpenConnectionAsync(trigger.TenantId, "writer", cancellationToken) .ConfigureAwait(false); await using var command = CreateCommand(sql, connection); diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj index 36c34892b..0d373f87d 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Persistence/StellaOps.Scheduler.Persistence.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Attestor/BundleRotationJob.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Attestor/BundleRotationJob.cs index 3333be5f0..3693ca347 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Attestor/BundleRotationJob.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Attestor/BundleRotationJob.cs @@ -227,15 +227,18 @@ public sealed class BundleRotationJob : IBundleRotationScheduler private readonly IAttestorBundleClient _bundleClient; private readonly BundleRotationOptions _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public BundleRotationJob( IAttestorBundleClient bundleClient, IOptions options, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _bundleClient = bundleClient ?? throw new ArgumentNullException(nameof(bundleClient)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -275,7 +278,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler string triggeredBy, CancellationToken ct = default) { - var startedAt = DateTimeOffset.UtcNow; + var startedAt = _timeProvider.GetUtcNow(); var results = new List(); var sw = Stopwatch.StartNew(); @@ -330,7 +333,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler } sw.Stop(); - var completedAt = DateTimeOffset.UtcNow; + var completedAt = _timeProvider.GetUtcNow(); var summary = new BundleRotationSummary( StartedAt: startedAt, @@ -417,7 +420,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler /// public async Task ApplyRetentionPolicyAsync(CancellationToken ct = default) { - var cutoffDate = DateTimeOffset.UtcNow.AddMonths(-_options.RetentionMonths); + var cutoffDate = _timeProvider.GetUtcNow().AddMonths(-_options.RetentionMonths); _logger.LogInformation( "Applying retention policy. Deleting bundles created before {Cutoff}", @@ -456,7 +459,7 @@ public sealed class BundleRotationJob : IBundleRotationScheduler private (DateTimeOffset start, DateTimeOffset end) GetCurrentBundlePeriod() { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); return _options.Cadence switch { diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/EvidenceBundleCoordinator.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/EvidenceBundleCoordinator.cs index ec02f62de..b4c33b0da 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/EvidenceBundleCoordinator.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/EvidenceBundleCoordinator.cs @@ -472,10 +472,17 @@ public sealed class InMemoryEvidenceBundleJobQueue : IEvidenceBundleJobQueue public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore { private readonly ConcurrentDictionary _bundles = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryEvidenceBundleStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public ValueTask StoreBundleAsync(string tenantId, string idempotencyKey, GeneratedBundle bundle, CancellationToken cancellationToken = default) { var key = $"{tenantId}:{idempotencyKey}"; + var now = _timeProvider.GetUtcNow(); var stored = new StoredBundle( bundle.BundleId, tenantId, @@ -483,8 +490,8 @@ public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore bundle.StorageUri, bundle.SizeBytes, BundleStatus.Completed, - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow.AddDays(7)); + now, + now.AddDays(7)); _bundles[key] = stored; return ValueTask.CompletedTask; @@ -498,7 +505,7 @@ public sealed class InMemoryEvidenceBundleStore : IEvidenceBundleStore public ValueTask CleanupExpiredAsync(TimeSpan maxAge, CancellationToken cancellationToken = default) { - var cutoff = DateTimeOffset.UtcNow - maxAge; + var cutoff = _timeProvider.GetUtcNow() - maxAge; var toRemove = _bundles .Where(kvp => kvp.Value.CreatedAt < cutoff) .Select(kvp => kvp.Key) @@ -551,8 +558,15 @@ public sealed class InMemoryJobManifestProvider : IJobManifestProvider /// public sealed class NullEvidenceBundleGenerator : IEvidenceBundleGenerator { + private readonly TimeProvider _timeProvider; + public static NullEvidenceBundleGenerator Instance { get; } = new(); + public NullEvidenceBundleGenerator(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + public ValueTask GenerateAsync(EvidenceBundleJob job, CancellationToken cancellationToken = default) { return ValueTask.FromResult(new GeneratedBundle( @@ -563,6 +577,6 @@ public sealed class NullEvidenceBundleGenerator : IEvidenceBundleGenerator ChecksumAlgorithm: "SHA256", BundleType: job.BundleType, ArtifactCount: job.ArtifactIds.Length, - GeneratedAt: DateTimeOffset.UtcNow)); + GeneratedAt: _timeProvider.GetUtcNow())); } } diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/ProgressStreamingWorker.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/ProgressStreamingWorker.cs index 8337a5fe1..a21675c87 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/ProgressStreamingWorker.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Console/ProgressStreamingWorker.cs @@ -345,15 +345,17 @@ public sealed class InMemoryProgressEventDeduplicator : IProgressEventDeduplicat { private readonly ConcurrentDictionary _processed = new(); private readonly TimeSpan _retentionPeriod; + private readonly TimeProvider _timeProvider; - public InMemoryProgressEventDeduplicator(TimeSpan? retentionPeriod = null) + public InMemoryProgressEventDeduplicator(TimeSpan? retentionPeriod = null, TimeProvider? timeProvider = null) { _retentionPeriod = retentionPeriod ?? TimeSpan.FromMinutes(30); + _timeProvider = timeProvider ?? TimeProvider.System; } public ValueTask TryMarkAsProcessedAsync(string eventId, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); // Clean up old entries periodically if (_processed.Count > 10000) diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs index d4eb490a3..22dc9b526 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Events/SchedulerEventPublisher.cs @@ -37,6 +37,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher private readonly INotifyEventQueue _queue; private readonly NotifyEventQueueOptions _queueOptions; private readonly TimeProvider _timeProvider; + private readonly StellaOps.Determinism.IGuidProvider _guidProvider; private readonly ILogger _logger; private readonly string _stream; @@ -44,11 +45,13 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher INotifyEventQueue queue, NotifyEventQueueOptions queueOptions, TimeProvider timeProvider, - ILogger logger) + ILogger logger, + StellaOps.Determinism.IGuidProvider? guidProvider = null) { _queue = queue ?? throw new ArgumentNullException(nameof(queue)); _queueOptions = queueOptions ?? throw new ArgumentNullException(nameof(queueOptions)); _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _stream = ResolveStream(queueOptions); } @@ -76,7 +79,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher var attributes = BuildReportAttributes(run, message, result, impactImage); var notifyEvent = NotifyEvent.Create( - eventId: Guid.NewGuid(), + eventId: _guidProvider.NewGuid(), kind: NotifyEventKinds.ScannerReportReady, tenant: run.TenantId, ts: occurredAt, @@ -110,7 +113,7 @@ internal sealed class SchedulerEventPublisher : ISchedulerEventPublisher var attributes = BuildRescanAttributes(run, message, deltas, impactLookup); var notifyEvent = NotifyEvent.Create( - eventId: Guid.NewGuid(), + eventId: _guidProvider.NewGuid(), kind: NotifyEventKinds.SchedulerRescanDelta, tenant: run.TenantId, ts: now, diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Exception/ExpiringNotificationWorker.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Exception/ExpiringNotificationWorker.cs index 35d0916d8..1550cac61 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Exception/ExpiringNotificationWorker.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Exception/ExpiringNotificationWorker.cs @@ -276,8 +276,17 @@ public sealed record ExpiringDigestEntry( /// public sealed class NullExpiringDigestService : IExpiringDigestService { + private readonly TimeProvider _timeProvider; + private readonly StellaOps.Determinism.IGuidProvider _guidProvider; + public static NullExpiringDigestService Instance { get; } = new(); + public NullExpiringDigestService(TimeProvider? timeProvider = null, StellaOps.Determinism.IGuidProvider? guidProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; + } + public ValueTask GenerateDigestAsync( string tenantId, IReadOnlyList expiringExceptions, @@ -285,9 +294,9 @@ public sealed class NullExpiringDigestService : IExpiringDigestService CancellationToken cancellationToken = default) { var digest = new ExpiringDigest( - DigestId: Guid.NewGuid().ToString("N"), + DigestId: _guidProvider.NewGuid().ToString("N"), TenantId: tenantId, - GeneratedAt: DateTimeOffset.UtcNow, + GeneratedAt: _timeProvider.GetUtcNow(), WindowEnd: windowEnd, TotalCount: expiringExceptions.Count, CriticalCount: 0, diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/HttpScannerReportClient.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/HttpScannerReportClient.cs index 894acaca9..ec3dd4427 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/HttpScannerReportClient.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/HttpScannerReportClient.cs @@ -21,15 +21,18 @@ internal sealed class HttpScannerReportClient : IScannerReportClient private readonly HttpClient _httpClient; private readonly IOptions _options; private readonly ILogger _logger; + private readonly StellaOps.Determinism.IGuidProvider _guidProvider; public HttpScannerReportClient( HttpClient httpClient, IOptions options, - ILogger logger) + ILogger logger, + StellaOps.Determinism.IGuidProvider? guidProvider = null) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; } public async Task ExecuteAsync( @@ -151,13 +154,13 @@ internal sealed class HttpScannerReportClient : IScannerReportClient private static bool IsTransient(Exception exception) => exception is HttpRequestException or TaskCanceledException; - private static RunnerReportSnapshot BuildReportSnapshot(ReportResponse report, string fallbackDigest) + private RunnerReportSnapshot BuildReportSnapshot(ReportResponse report, string fallbackDigest) { var document = report.Report ?? new ReportDocument(); var summary = document.Summary ?? new ReportSummary(); return new RunnerReportSnapshot( - string.IsNullOrWhiteSpace(document.ReportId) ? Guid.NewGuid().ToString("N") : document.ReportId, + string.IsNullOrWhiteSpace(document.ReportId) ? _guidProvider.NewGuid().ToString("N") : document.ReportId, string.IsNullOrWhiteSpace(document.ImageDigest) ? fallbackDigest : document.ImageDigest, string.IsNullOrWhiteSpace(document.Verdict) ? "warn" : document.Verdict, document.GeneratedAt, diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/PartitionHealthMonitor.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/PartitionHealthMonitor.cs index d3a404b1e..08e52d065 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/PartitionHealthMonitor.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Execution/PartitionHealthMonitor.cs @@ -48,14 +48,17 @@ public sealed class PartitionHealthMonitor /// PostgreSQL connection. /// Days threshold for warning alert. /// Days threshold for critical alert. + /// Optional time provider for deterministic testing. /// Cancellation token. /// List of partition health status for each table. public async Task> CheckHealthAsync( NpgsqlConnection connection, int alertThreshold = 30, int criticalThreshold = 7, + TimeProvider? timeProvider = null, CancellationToken cancellationToken = default) { + var time = timeProvider ?? TimeProvider.System; using var activity = ActivitySource.StartActivity("partitions.health_check", ActivityKind.Internal); var results = new List(); @@ -82,6 +85,7 @@ public sealed class PartitionHealthMonitor { await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + var now = time.GetUtcNow(); while (await reader.ReadAsync(cancellationToken)) { var schema = reader.GetString(0); @@ -91,7 +95,7 @@ public sealed class PartitionHealthMonitor var tableKey = $"{schema}.{table}"; var daysUntilExhaustion = lastPartitionStart.HasValue - ? Math.Max(0, (int)(lastPartitionStart.Value - DateTimeOffset.UtcNow).TotalDays) + ? Math.Max(0, (int)(lastPartitionStart.Value - now).TotalDays) : 0; futureCounts[tableKey] = futureCount; diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Planning/ScoreReplaySchedulerJob.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Planning/ScoreReplaySchedulerJob.cs index 7e2ad1a90..18ab656b1 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Planning/ScoreReplaySchedulerJob.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Planning/ScoreReplaySchedulerJob.cs @@ -134,17 +134,20 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler { private readonly IScannerReplayClient _scannerClient; private readonly ScoreReplaySchedulerOptions _options; + private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private string? _lastFeedSnapshotHash; public ScoreReplaySchedulerJob( IScannerReplayClient scannerClient, IOptions options, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _scannerClient = scannerClient ?? throw new ArgumentNullException(nameof(scannerClient)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -191,7 +194,7 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler string? feedSnapshotHash = null, CancellationToken ct = default) { - var startedAt = DateTimeOffset.UtcNow; + var startedAt = _timeProvider.GetUtcNow(); var results = new List(); var successCount = 0; var failureCount = 0; @@ -252,7 +255,7 @@ public sealed class ScoreReplaySchedulerJob : IScoreReplayScheduler _logger.LogError(ex, "Error during batch score replay"); } - var completedAt = DateTimeOffset.UtcNow; + var completedAt = _timeProvider.GetUtcNow(); _logger.LogInformation( "Score replay batch completed. Success={Success}, Failed={Failed}, SignificantDeltas={Deltas}, Duration={Duration}ms", diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Policy/GateEvaluationJob.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Policy/GateEvaluationJob.cs index 10715500b..c04401d1c 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Policy/GateEvaluationJob.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Policy/GateEvaluationJob.cs @@ -200,6 +200,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler private readonly IPolicyGatewayClient _gatewayClient; private readonly GateEvaluationOptions _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; // In-memory queue for pending jobs (replace with persistent store in production) private readonly Queue _pendingJobs = new(); @@ -209,11 +210,13 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler public GateEvaluationJob( IPolicyGatewayClient gatewayClient, IOptions options, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _gatewayClient = gatewayClient ?? throw new ArgumentNullException(nameof(gatewayClient)); _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -233,7 +236,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler DeltaCount: null, CriticalCount: null, HighCount: null, - StartedAt: DateTimeOffset.UtcNow, + StartedAt: _timeProvider.GetUtcNow(), CompletedAt: default, Duration: TimeSpan.Zero); } @@ -262,13 +265,13 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler { _logger.LogDebug("Gate evaluation jobs are disabled"); return new GateEvaluationBatchSummary( - DateTimeOffset.UtcNow, - DateTimeOffset.UtcNow, + _timeProvider.GetUtcNow(), + _timeProvider.GetUtcNow(), 0, 0, 0, 0, 0, TimeSpan.Zero); } - var startedAt = DateTimeOffset.UtcNow; + var startedAt = _timeProvider.GetUtcNow(); var sw = Stopwatch.StartNew(); var results = new List(); @@ -292,7 +295,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler results.AddRange(completedResults); sw.Stop(); - var completedAt = DateTimeOffset.UtcNow; + var completedAt = _timeProvider.GetUtcNow(); var summary = new GateEvaluationBatchSummary( StartedAt: startedAt, @@ -320,7 +323,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler GateEvaluationRequest request, CancellationToken ct) { - var startedAt = DateTimeOffset.UtcNow; + var startedAt = _timeProvider.GetUtcNow(); var sw = Stopwatch.StartNew(); // Update status to in-progress @@ -347,7 +350,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler timeoutCts.Token); sw.Stop(); - var completedAt = DateTimeOffset.UtcNow; + var completedAt = _timeProvider.GetUtcNow(); var result = new GateEvaluationResult( JobId: request.JobId, @@ -453,7 +456,7 @@ public sealed class GateEvaluationJob : IGateEvaluationScheduler CriticalCount: null, HighCount: null, StartedAt: startedAt, - CompletedAt: DateTimeOffset.UtcNow, + CompletedAt: _timeProvider.GetUtcNow(), Duration: duration, ErrorMessage: errorMessage); diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityJoinerWorker.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityJoinerWorker.cs index a9b819363..ab14a34c1 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityJoinerWorker.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityJoinerWorker.cs @@ -186,7 +186,7 @@ public sealed class ReachabilityJoinerWorker : BackgroundService } } - private static IReadOnlyList JoinSnapshotWithSignals( + private IReadOnlyList JoinSnapshotWithSignals( SbomSnapshot snapshot, IReadOnlyDictionary signals) { @@ -207,7 +207,7 @@ public sealed class ReachabilityJoinerWorker : BackgroundService IsReachable: signal.IsReachable, Confidence: signal.Confidence, Evidence: signal.Evidence, - ProducedAt: DateTimeOffset.UtcNow); + ProducedAt: _timeProvider.GetUtcNow()); facts.Add(fact); } @@ -384,6 +384,12 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache { private readonly Dictionary Facts, DateTimeOffset ExpiresAt)> _cache = new(); private readonly object _lock = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryReachabilityFactCache(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public ValueTask WriteFactsAsync( string tenantId, @@ -396,7 +402,7 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache lock (_lock) { - _cache[key] = (facts, DateTimeOffset.UtcNow.Add(ttl)); + _cache[key] = (facts, _timeProvider.GetUtcNow().Add(ttl)); } return ValueTask.CompletedTask; @@ -411,7 +417,7 @@ public sealed class InMemoryReachabilityFactCache : IReachabilityFactCache lock (_lock) { - if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTimeOffset.UtcNow) + if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > _timeProvider.GetUtcNow()) { return ValueTask.FromResult(entry.Facts); } diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityStalenessMonitor.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityStalenessMonitor.cs index 7fe67f0de..d99217c2e 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityStalenessMonitor.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Reachability/ReachabilityStalenessMonitor.cs @@ -291,6 +291,12 @@ public sealed class InMemoryReachabilityFactStore : IReachabilityFactStore { private readonly Dictionary> _facts = new(); private readonly object _lock = new(); + private readonly TimeProvider _timeProvider; + + public InMemoryReachabilityFactStore(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public ValueTask> GetTenantsWithFactsAsync( CancellationToken cancellationToken = default) @@ -343,7 +349,7 @@ public sealed class InMemoryReachabilityFactStore : IReachabilityFactStore int maxCount, CancellationToken cancellationToken = default) { - var now = DateTimeOffset.UtcNow; + var now = _timeProvider.GetUtcNow(); lock (_lock) { diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Resolver/EvaluationOrchestrationWorker.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Resolver/EvaluationOrchestrationWorker.cs index 4b593a4b5..c5067b6af 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Resolver/EvaluationOrchestrationWorker.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Resolver/EvaluationOrchestrationWorker.cs @@ -422,14 +422,24 @@ public sealed class InMemoryFindingsLedgerProjector : IFindingsLedgerProjector /// public sealed class NullPolicyEngineEvaluator : IPolicyEngineEvaluator { + private readonly TimeProvider _timeProvider; + private readonly StellaOps.Determinism.IGuidProvider _guidProvider; + public static NullPolicyEngineEvaluator Instance { get; } = new(); + public NullPolicyEngineEvaluator(TimeProvider? timeProvider = null, StellaOps.Determinism.IGuidProvider? guidProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance; + } + public ValueTask EvaluateBatchAsync( string tenantId, string artifactId, IReadOnlyList candidates, CancellationToken cancellationToken = default) { + var now = _timeProvider.GetUtcNow(); var evaluatedFindings = candidates .Select(c => new EvaluatedFinding( c.FindingId, @@ -440,13 +450,13 @@ public sealed class NullPolicyEngineEvaluator : IPolicyEngineEvaluator "default-policy", null, null, - DateTimeOffset.UtcNow)) + now)) .ToImmutableArray(); return ValueTask.FromResult(new BatchEvaluationResult( - BatchId: Guid.NewGuid().ToString("N"), + BatchId: _guidProvider.NewGuid().ToString("N"), EvaluatedFindings: evaluatedFindings, SkippedCount: 0, - EvaluatedAt: DateTimeOffset.UtcNow)); + EvaluatedAt: now)); } } diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationReducerWorker.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationReducerWorker.cs index 4821315a3..a4c5362ea 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationReducerWorker.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationReducerWorker.cs @@ -446,6 +446,12 @@ public sealed class InMemorySimulationManifestWriter : ISimulationManifestWriter { private readonly Dictionary _manifests = new(); private readonly object _lock = new(); + private readonly TimeProvider _timeProvider; + + public InMemorySimulationManifestWriter(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } public ValueTask WriteManifestAsync( string tenantId, @@ -462,7 +468,7 @@ public sealed class InMemorySimulationManifestWriter : ISimulationManifestWriter Checksum: checksum, ChecksumAlgorithm: "SHA256", SizeBytes: bytes.Length, - StoredAt: DateTimeOffset.UtcNow); + StoredAt: _timeProvider.GetUtcNow()); lock (_lock) { diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationSecurityEnforcer.cs b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationSecurityEnforcer.cs index a53a063bb..28fd70b65 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationSecurityEnforcer.cs +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/Simulation/SimulationSecurityEnforcer.cs @@ -17,19 +17,22 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer private readonly ISecretScanner _secretScanner; private readonly SchedulerWorkerOptions _options; private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; public SimulationSecurityEnforcer( ITenantScopeValidator scopeValidator, IAttestationVerifier attestationVerifier, ISecretScanner secretScanner, SchedulerWorkerOptions options, - ILogger logger) + ILogger logger, + TimeProvider? timeProvider = null) { _scopeValidator = scopeValidator ?? throw new ArgumentNullException(nameof(scopeValidator)); _attestationVerifier = attestationVerifier ?? throw new ArgumentNullException(nameof(attestationVerifier)); _secretScanner = secretScanner ?? throw new ArgumentNullException(nameof(secretScanner)); _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; } /// @@ -77,7 +80,7 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer return new SecurityValidationResult( IsValid: isValid, Violations: [.. violations], - ValidatedAt: DateTimeOffset.UtcNow, + ValidatedAt: _timeProvider.GetUtcNow(), ValidatorVersion: "1.0.0"); } @@ -112,7 +115,7 @@ public sealed class SimulationSecurityEnforcer : ISimulationSecurityEnforcer return new SecurityValidationResult( IsValid: violations.Count == 0, Violations: [.. violations], - ValidatedAt: DateTimeOffset.UtcNow, + ValidatedAt: _timeProvider.GetUtcNow(), ValidatorVersion: "1.0.0"); } diff --git a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/StellaOps.Scheduler.Worker.csproj b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/StellaOps.Scheduler.Worker.csproj index 7a3edb402..1c72e50b7 100644 --- a/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/StellaOps.Scheduler.Worker.csproj +++ b/src/Scheduler/__Libraries/StellaOps.Scheduler.Worker/StellaOps.Scheduler.Worker.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/GraphJobServiceTests.cs b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/GraphJobServiceTests.cs index 09347aaa9..c9623fc78 100644 --- a/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/GraphJobServiceTests.cs +++ b/src/Scheduler/__Tests/StellaOps.Scheduler.WebService.Tests/GraphJobServiceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; using StellaOps.Scheduler.Models; using StellaOps.Scheduler.WebService.GraphJobs; using Xunit; @@ -21,10 +22,10 @@ public sealed class GraphJobServiceTests var initial = CreateBuildJob(); await store.AddAsync(initial, CancellationToken.None); - var clock = new FixedClock(FixedTime); + var timeProvider = new FakeTimeProvider(FixedTime); var publisher = new RecordingPublisher(); var webhook = new RecordingWebhookClient(); - var service = new GraphJobService(store, clock, publisher, webhook); + var service = new GraphJobService(store, timeProvider, publisher, webhook); var request = new GraphJobCompletionRequest { @@ -60,10 +61,10 @@ public sealed class GraphJobServiceTests var initial = CreateBuildJob(); await store.AddAsync(initial, CancellationToken.None); - var clock = new FixedClock(FixedTime); + var timeProvider = new FakeTimeProvider(FixedTime); var publisher = new RecordingPublisher(); var webhook = new RecordingWebhookClient(); - var service = new GraphJobService(store, clock, publisher, webhook); + var service = new GraphJobService(store, timeProvider, publisher, webhook); var request = new GraphJobCompletionRequest { @@ -95,10 +96,10 @@ public sealed class GraphJobServiceTests var initial = CreateBuildJob(); await store.AddAsync(initial, CancellationToken.None); - var clock = new FixedClock(FixedTime); + var timeProvider = new FakeTimeProvider(FixedTime); var publisher = new RecordingPublisher(); var webhook = new RecordingWebhookClient(); - var service = new GraphJobService(store, clock, publisher, webhook); + var service = new GraphJobService(store, timeProvider, publisher, webhook); var firstRequest = new GraphJobCompletionRequest { @@ -140,10 +141,10 @@ public sealed class GraphJobServiceTests public async Task CreateBuildJob_NormalizesSbomDigest() { var store = new TrackingGraphJobStore(); - var clock = new FixedClock(FixedTime); + var timeProvider = new FakeTimeProvider(FixedTime); var publisher = new RecordingPublisher(); var webhook = new RecordingWebhookClient(); - var service = new GraphJobService(store, clock, publisher, webhook); + var service = new GraphJobService(store, timeProvider, publisher, webhook); var request = new GraphBuildJobRequest { @@ -161,10 +162,10 @@ public sealed class GraphJobServiceTests public async Task CreateBuildJob_RejectsDigestWithoutPrefix() { var store = new TrackingGraphJobStore(); - var clock = new FixedClock(FixedTime); + var timeProvider = new FakeTimeProvider(FixedTime); var publisher = new RecordingPublisher(); var webhook = new RecordingWebhookClient(); - var service = new GraphJobService(store, clock, publisher, webhook); + var service = new GraphJobService(store, timeProvider, publisher, webhook); var request = new GraphBuildJobRequest { @@ -253,14 +254,4 @@ public sealed class GraphJobServiceTests return Task.CompletedTask; } } - - private sealed class FixedClock : ISystemClock - { - public FixedClock(DateTimeOffset utcNow) - { - UtcNow = utcNow; - } - - public DateTimeOffset UtcNow { get; set; } - } }