Add post-quantum cryptography support with PqSoftCryptoProvider
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled

- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle.
- Added PqSoftProviderOptions and PqSoftKeyOptions for configuration.
- Created unit tests for Dilithium3 and Falcon512 signing and verification.
- Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists.
- Added KcmvpHashOnlyProvider for KCMVP baseline compliance.
- Updated project files and dependencies for new libraries and testing frameworks.
This commit is contained in:
StellaOps Bot
2025-12-07 15:04:19 +02:00
parent 862bb6ed80
commit 98e6b76584
119 changed files with 11436 additions and 1732 deletions

View File

@@ -0,0 +1,64 @@
# Findings Ledger Docker Compose overlay
# Append to or reference from your main compose file
#
# Usage:
# docker compose -f docker-compose.yaml -f ops/devops/findings-ledger/compose/docker-compose.ledger.yaml up -d
services:
findings-ledger:
image: stellaops/findings-ledger:${STELLA_VERSION:-2025.11.0}
restart: unless-stopped
env_file:
- ./env/ledger.${STELLAOPS_ENV:-dev}.env
environment:
ASPNETCORE_URLS: http://0.0.0.0:8080
ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production}
# Database connection (override via env file or secrets)
# LEDGER__DB__CONNECTIONSTRING: see secrets
# Observability
LEDGER__OBSERVABILITY__ENABLED: "true"
LEDGER__OBSERVABILITY__OTLPENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4317}
# Merkle anchoring
LEDGER__MERKLE__ANCHORINTERVAL: "00:05:00"
LEDGER__MERKLE__EXTERNALIZE: ${LEDGER_MERKLE_EXTERNALIZE:-false}
# Attachments
LEDGER__ATTACHMENTS__MAXSIZEBYTES: "104857600" # 100MB
LEDGER__ATTACHMENTS__ALLOWEGRESS: ${LEDGER_ATTACHMENTS_ALLOWEGRESS:-true}
ports:
- "${LEDGER_PORT:-8188}:8080"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8080/health/ready"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
volumes:
- ledger-data:/app/data
- ./etc/ledger/appsettings.json:/app/appsettings.json:ro
networks:
- stellaops
# Migration job (run before starting ledger)
findings-ledger-migrations:
image: stellaops/findings-ledger-migrations:${STELLA_VERSION:-2025.11.0}
command: ["--connection", "${LEDGER__DB__CONNECTIONSTRING}"]
env_file:
- ./env/ledger.${STELLAOPS_ENV:-dev}.env
depends_on:
postgres:
condition: service_healthy
networks:
- stellaops
profiles:
- migrations
volumes:
ledger-data:
driver: local
networks:
stellaops:
external: true

View File

@@ -0,0 +1,24 @@
# Findings Ledger - Development Environment
# Copy to ledger.local.env and customize for local dev
# Database connection
LEDGER__DB__CONNECTIONSTRING=Host=postgres;Port=5432;Database=findings_ledger_dev;Username=ledger;Password=change_me_dev;
# Attachment encryption key (AES-256, base64 encoded)
# Generate with: openssl rand -base64 32
LEDGER__ATTACHMENTS__ENCRYPTIONKEY=
# Merkle anchor signing (optional in dev)
LEDGER__MERKLE__SIGNINGKEY=
# Authority service endpoint (for JWT validation)
LEDGER__AUTHORITY__BASEURL=http://authority:8080
# Logging level
Logging__LogLevel__Default=Debug
Logging__LogLevel__Microsoft=Information
Logging__LogLevel__StellaOps=Debug
# Feature flags
LEDGER__FEATURES__ENABLEATTACHMENTS=true
LEDGER__FEATURES__ENABLEAUDITLOG=true

View File

@@ -0,0 +1,40 @@
# Findings Ledger - Production Environment
# Secrets should be injected from secrets manager, not committed
# Database connection (inject from secrets manager)
# LEDGER__DB__CONNECTIONSTRING=
# Attachment encryption key (inject from secrets manager)
# LEDGER__ATTACHMENTS__ENCRYPTIONKEY=
# Merkle anchor signing (inject from secrets manager)
# LEDGER__MERKLE__SIGNINGKEY=
# Authority service endpoint
LEDGER__AUTHORITY__BASEURL=http://authority:8080
# Logging level
Logging__LogLevel__Default=Warning
Logging__LogLevel__Microsoft=Warning
Logging__LogLevel__StellaOps=Information
# Feature flags
LEDGER__FEATURES__ENABLEATTACHMENTS=true
LEDGER__FEATURES__ENABLEAUDITLOG=true
# Observability
LEDGER__OBSERVABILITY__ENABLED=true
LEDGER__OBSERVABILITY__METRICSPORT=9090
# Merkle anchoring
LEDGER__MERKLE__ANCHORINTERVAL=00:05:00
LEDGER__MERKLE__EXTERNALIZE=false
# Attachments
LEDGER__ATTACHMENTS__MAXSIZEBYTES=104857600
LEDGER__ATTACHMENTS__ALLOWEGRESS=false
# Air-gap staleness thresholds (seconds)
LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD=604800
LEDGER__AIRGAP__VEXSTALETHRESHOLD=604800
LEDGER__AIRGAP__POLICYSTALETHRESHOLD=86400

View File

@@ -0,0 +1,20 @@
apiVersion: v2
name: stellaops-findings-ledger
version: 0.1.0
appVersion: "2025.11.0"
description: Findings Ledger service for StellaOps platform - event-sourced findings storage with Merkle anchoring.
type: application
keywords:
- findings
- ledger
- event-sourcing
- merkle
- attestation
maintainers:
- name: StellaOps Team
email: platform@stellaops.io
dependencies:
- name: postgresql
version: "14.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled

View File

@@ -0,0 +1,80 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "findings-ledger.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "findings-ledger.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "findings-ledger.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "findings-ledger.labels" -}}
helm.sh/chart: {{ include "findings-ledger.chart" . }}
{{ include "findings-ledger.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "findings-ledger.selectorLabels" -}}
app.kubernetes.io/name: {{ include "findings-ledger.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: ledger
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "findings-ledger.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "findings-ledger.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Database connection string - from secret or constructed
*/}}
{{- define "findings-ledger.databaseConnectionString" -}}
{{- if .Values.database.connectionStringSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.database.connectionStringSecret }}
key: {{ .Values.database.connectionStringKey }}
{{- else if .Values.postgresql.enabled }}
value: "Host={{ .Release.Name }}-postgresql;Port=5432;Database={{ .Values.postgresql.auth.database }};Username={{ .Values.postgresql.auth.username }};Password=$(POSTGRES_PASSWORD);"
{{- else }}
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: LEDGER__DB__CONNECTIONSTRING
{{- end }}
{{- end }}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "findings-ledger.fullname" . }}-config
labels:
{{- include "findings-ledger.labels" . | nindent 4 }}
data:
appsettings.json: |
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"StellaOps": "Information"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,122 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "findings-ledger.fullname" . }}
labels:
{{- include "findings-ledger.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "findings-ledger.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "findings-ledger.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "findings-ledger.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: ledger
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.observability.metricsEnabled }}
- name: metrics
containerPort: {{ .Values.service.metricsPort }}
protocol: TCP
{{- end }}
env:
- name: ASPNETCORE_URLS
value: "http://0.0.0.0:{{ .Values.service.port }}"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
# Database
- name: LEDGER__DB__CONNECTIONSTRING
{{- include "findings-ledger.databaseConnectionString" . | nindent 14 }}
# Observability
- name: LEDGER__OBSERVABILITY__ENABLED
value: {{ .Values.observability.enabled | quote }}
- name: LEDGER__OBSERVABILITY__OTLPENDPOINT
value: {{ .Values.observability.otlpEndpoint | quote }}
# Merkle anchoring
- name: LEDGER__MERKLE__ANCHORINTERVAL
value: {{ .Values.merkle.anchorInterval | quote }}
- name: LEDGER__MERKLE__EXTERNALIZE
value: {{ .Values.merkle.externalize | quote }}
# Attachments
- name: LEDGER__ATTACHMENTS__MAXSIZEBYTES
value: {{ .Values.attachments.maxSizeBytes | quote }}
- name: LEDGER__ATTACHMENTS__ALLOWEGRESS
value: {{ .Values.attachments.allowEgress | quote }}
- name: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.name }}
key: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
# Authority
- name: LEDGER__AUTHORITY__BASEURL
value: {{ .Values.authority.baseUrl | quote }}
# Air-gap thresholds
- name: LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD
value: {{ .Values.airgap.advisoryStaleThreshold | quote }}
- name: LEDGER__AIRGAP__VEXSTALETHRESHOLD
value: {{ .Values.airgap.vexStaleThreshold | quote }}
- name: LEDGER__AIRGAP__POLICYSTALETHRESHOLD
value: {{ .Values.airgap.policyStaleThreshold | quote }}
# Features
- name: LEDGER__FEATURES__ENABLEATTACHMENTS
value: {{ .Values.features.enableAttachments | quote }}
- name: LEDGER__FEATURES__ENABLEAUDITLOG
value: {{ .Values.features.enableAuditLog | quote }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.extraEnvFrom }}
envFrom:
{{- toYaml . | nindent 12 }}
{{- end }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: tmp
mountPath: /tmp
- name: data
mountPath: /app/data
volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,43 @@
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "findings-ledger.fullname" . }}-migrations
labels:
{{- include "findings-ledger.labels" . | nindent 4 }}
app.kubernetes.io/component: migrations
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 3
template:
metadata:
labels:
{{- include "findings-ledger.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: migrations
spec:
serviceAccountName: {{ include "findings-ledger.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
restartPolicy: Never
containers:
- name: migrations
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.migrations.image.repository }}:{{ .Values.migrations.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
args:
- "--connection"
- "$(LEDGER__DB__CONNECTIONSTRING)"
env:
- name: LEDGER__DB__CONNECTIONSTRING
{{- include "findings-ledger.databaseConnectionString" . | nindent 14 }}
resources:
{{- toYaml .Values.migrations.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "findings-ledger.fullname" . }}
labels:
{{- include "findings-ledger.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
{{- if .Values.observability.metricsEnabled }}
- port: {{ .Values.service.metricsPort }}
targetPort: metrics
protocol: TCP
name: metrics
{{- end }}
selector:
{{- include "findings-ledger.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "findings-ledger.serviceAccountName" . }}
labels:
{{- include "findings-ledger.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,151 @@
# Default values for stellaops-findings-ledger
image:
repository: stellaops/findings-ledger
tag: "2025.11.0"
pullPolicy: IfNotPresent
replicaCount: 1
service:
type: ClusterIP
port: 8080
metricsPort: 9090
# Database configuration
database:
# External PostgreSQL connection (preferred for production)
# Set connectionStringSecret to use existing secret
connectionStringSecret: ""
connectionStringKey: "LEDGER__DB__CONNECTIONSTRING"
# Or provide connection details directly (not recommended for prod)
host: "postgres"
port: 5432
database: "findings_ledger"
username: "ledger"
# password via secret only
# Built-in PostgreSQL (dev/testing only)
postgresql:
enabled: false
auth:
username: ledger
database: findings_ledger
# Secrets configuration
secrets:
# Name of secret containing sensitive values
name: "findings-ledger-secrets"
# Expected keys in secret:
# LEDGER__DB__CONNECTIONSTRING
# LEDGER__ATTACHMENTS__ENCRYPTIONKEY
# LEDGER__MERKLE__SIGNINGKEY (optional)
# Observability
observability:
enabled: true
otlpEndpoint: "http://otel-collector:4317"
metricsEnabled: true
# Merkle anchoring
merkle:
anchorInterval: "00:05:00"
externalize: false
# externalAnchorEndpoint: ""
# Attachments
attachments:
maxSizeBytes: 104857600 # 100MB
allowEgress: true
# encryptionKey via secret
# Air-gap configuration
airgap:
advisoryStaleThreshold: 604800 # 7 days
vexStaleThreshold: 604800 # 7 days
policyStaleThreshold: 86400 # 1 day
# Authority integration
authority:
baseUrl: "http://authority:8080"
# Feature flags
features:
enableAttachments: true
enableAuditLog: true
# Resource limits
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
# Probes
probes:
readiness:
path: /health/ready
initialDelaySeconds: 10
periodSeconds: 10
liveness:
path: /health/live
initialDelaySeconds: 15
periodSeconds: 20
# Pod configuration
nodeSelector: {}
tolerations: []
affinity: {}
# Extra environment variables
extraEnv: []
# - name: CUSTOM_VAR
# value: "value"
extraEnvFrom: []
# - secretRef:
# name: additional-secrets
# Migration job
migrations:
enabled: true
image:
repository: stellaops/findings-ledger-migrations
tag: "2025.11.0"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
# Service account
serviceAccount:
create: true
name: ""
annotations: {}
# Pod security context
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Container security context
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
# Ingress (optional)
ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []

View File

@@ -0,0 +1,158 @@
# Findings Ledger Offline Kit
This directory contains manifests and scripts for deploying Findings Ledger in air-gapped/offline environments.
## Contents
```
offline-kit/
├── README.md # This file
├── manifest.yaml # Offline bundle manifest
├── images/ # Container image tarballs (populated at build)
│ └── .gitkeep
├── migrations/ # Database migration scripts
│ └── .gitkeep
├── dashboards/ # Grafana dashboard JSON exports
│ └── findings-ledger.json
├── alerts/ # Prometheus alert rules
│ └── findings-ledger-alerts.yaml
└── scripts/
├── import-images.sh # Load container images
├── run-migrations.sh # Apply database migrations
└── verify-install.sh # Post-install verification
```
## Building the Offline Kit
Use the platform offline kit builder:
```bash
# From repository root
python ops/offline-kit/build_offline_kit.py \
--include ledger \
--version 2025.11.0 \
--output dist/offline-kit-ledger-2025.11.0.tar.gz
```
## Installation Steps
### 1. Transfer and Extract
```bash
# On air-gapped host
tar xzf offline-kit-ledger-*.tar.gz
cd offline-kit-ledger-*
```
### 2. Load Container Images
```bash
./scripts/import-images.sh
# Loads: stellaops/findings-ledger, stellaops/findings-ledger-migrations
```
### 3. Run Database Migrations
```bash
export LEDGER__DB__CONNECTIONSTRING="Host=...;Database=...;..."
./scripts/run-migrations.sh
```
### 4. Deploy Service
Choose deployment method:
**Docker Compose:**
```bash
cp ../compose/env/ledger.prod.env ./ledger.env
# Edit ledger.env with local values
docker compose -f ../compose/docker-compose.ledger.yaml up -d
```
**Helm:**
```bash
helm upgrade --install findings-ledger ../helm \
-f values-offline.yaml \
--set image.pullPolicy=Never
```
### 5. Verify Installation
```bash
./scripts/verify-install.sh
```
## Configuration Notes
### Sealed Mode
In air-gapped environments, configure:
```yaml
# Disable outbound attachment egress
LEDGER__ATTACHMENTS__ALLOWEGRESS: "false"
# Set appropriate staleness thresholds
LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD: "604800" # 7 days
LEDGER__AIRGAP__VEXSTALETHRESHOLD: "604800"
LEDGER__AIRGAP__POLICYSTALETHRESHOLD: "86400" # 1 day
```
### Merkle Anchoring
For offline environments without external anchoring:
```yaml
LEDGER__MERKLE__EXTERNALIZE: "false"
```
Keep local Merkle roots and export periodically for audit.
## Backup & Restore
See `docs/modules/findings-ledger/deployment.md` for full backup/restore procedures.
Quick reference:
```bash
# Backup
pg_dump -Fc --dbname="$LEDGER_DB" --file ledger-$(date -u +%Y%m%d).dump
# Restore
pg_restore -C -d postgres ledger-YYYYMMDD.dump
# Replay projections
dotnet run --project tools/LedgerReplayHarness -- \
--connection "$LEDGER_DB" --tenant all
```
## Observability
Import the provided dashboards into your local Grafana instance:
```bash
# Import via Grafana API or UI
curl -X POST http://grafana:3000/api/dashboards/db \
-H "Content-Type: application/json" \
-d @dashboards/findings-ledger.json
```
Apply alert rules to Prometheus:
```bash
cp alerts/findings-ledger-alerts.yaml /etc/prometheus/rules.d/
# Reload Prometheus
```
## Troubleshooting
| Issue | Resolution |
| --- | --- |
| Migration fails | Check DB connectivity; verify user has CREATE/ALTER privileges |
| Health check fails | Check logs: `docker logs findings-ledger` or `kubectl logs -l app.kubernetes.io/name=findings-ledger` |
| Metrics not visible | Verify OTLP endpoint is reachable or use Prometheus scrape |
| Staleness warnings | Import fresh advisory/VEX bundles via Mirror |
## Support
- Platform docs: `docs/modules/findings-ledger/`
- Offline operation: `docs/24_OFFLINE_KIT.md`
- Air-gap mode: `docs/airgap/`

View File

@@ -0,0 +1,122 @@
# Findings Ledger Prometheus Alert Rules
# Apply to Prometheus: cp findings-ledger-alerts.yaml /etc/prometheus/rules.d/
groups:
- name: findings-ledger
rules:
# Service availability
- alert: FindingsLedgerDown
expr: up{job="findings-ledger"} == 0
for: 2m
labels:
severity: critical
service: findings-ledger
annotations:
summary: "Findings Ledger service is down"
description: "Findings Ledger service has been unreachable for more than 2 minutes."
# Write latency
- alert: FindingsLedgerHighWriteLatency
expr: histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job="findings-ledger"}[5m])) by (le)) > 1
for: 5m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger write latency is high"
description: "95th percentile write latency exceeds 1 second for 5 minutes. Current: {{ $value | humanizeDuration }}"
- alert: FindingsLedgerCriticalWriteLatency
expr: histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job="findings-ledger"}[5m])) by (le)) > 5
for: 2m
labels:
severity: critical
service: findings-ledger
annotations:
summary: "Findings Ledger write latency is critically high"
description: "95th percentile write latency exceeds 5 seconds. Current: {{ $value | humanizeDuration }}"
# Projection lag
- alert: FindingsLedgerProjectionLag
expr: ledger_projection_lag_seconds{job="findings-ledger"} > 30
for: 5m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger projection lag is high"
description: "Projection lag exceeds 30 seconds for 5 minutes. Current: {{ $value | humanizeDuration }}"
- alert: FindingsLedgerCriticalProjectionLag
expr: ledger_projection_lag_seconds{job="findings-ledger"} > 300
for: 2m
labels:
severity: critical
service: findings-ledger
annotations:
summary: "Findings Ledger projection lag is critically high"
description: "Projection lag exceeds 5 minutes. Current: {{ $value | humanizeDuration }}"
# Merkle anchoring
- alert: FindingsLedgerMerkleAnchorStale
expr: time() - ledger_merkle_last_anchor_timestamp_seconds{job="findings-ledger"} > 600
for: 5m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger Merkle anchor is stale"
description: "No Merkle anchor created in the last 10 minutes. Last anchor: {{ $value | humanizeTimestamp }}"
- alert: FindingsLedgerMerkleAnchorFailed
expr: increase(ledger_merkle_anchor_failures_total{job="findings-ledger"}[15m]) > 0
for: 0m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger Merkle anchoring failed"
description: "Merkle anchor operation failed. Check logs for details."
# Database connectivity
- alert: FindingsLedgerDatabaseErrors
expr: increase(ledger_database_errors_total{job="findings-ledger"}[5m]) > 5
for: 2m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger database errors detected"
description: "More than 5 database errors in the last 5 minutes."
# Attachment storage
- alert: FindingsLedgerAttachmentStorageErrors
expr: increase(ledger_attachment_storage_errors_total{job="findings-ledger"}[15m]) > 0
for: 0m
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Findings Ledger attachment storage errors"
description: "Attachment storage operation failed. Check encryption keys and storage connectivity."
# Air-gap staleness (for offline environments)
- alert: FindingsLedgerAdvisoryStaleness
expr: ledger_airgap_advisory_staleness_seconds{job="findings-ledger"} > 604800
for: 1h
labels:
severity: warning
service: findings-ledger
annotations:
summary: "Advisory data is stale in air-gapped environment"
description: "Advisory data is older than 7 days. Import fresh data from Mirror."
- alert: FindingsLedgerVexStaleness
expr: ledger_airgap_vex_staleness_seconds{job="findings-ledger"} > 604800
for: 1h
labels:
severity: warning
service: findings-ledger
annotations:
summary: "VEX data is stale in air-gapped environment"
description: "VEX data is older than 7 days. Import fresh data from Mirror."

View File

@@ -0,0 +1,185 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "9.0.0"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
}
],
"annotations": {
"list": []
},
"description": "Findings Ledger service metrics and health",
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 1,
"panels": [],
"title": "Health Overview",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] }
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"id": 2,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"pluginVersion": "9.0.0",
"targets": [{ "expr": "up{job=\"findings-ledger\"}", "refId": "A" }],
"title": "Service Status",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "palette-classic" }, "unit": "short" },
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
"id": 3,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"pluginVersion": "9.0.0",
"targets": [{ "expr": "ledger_events_total{job=\"findings-ledger\"}", "refId": "A" }],
"title": "Total Events",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "thresholds" }, "unit": "s", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }, { "color": "red", "value": 5 }] } },
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 },
"id": 4,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"pluginVersion": "9.0.0",
"targets": [{ "expr": "ledger_projection_lag_seconds{job=\"findings-ledger\"}", "refId": "A" }],
"title": "Projection Lag",
"type": "stat"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 10,
"panels": [],
"title": "Write Performance",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "s" },
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
"id": 11,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
"pluginVersion": "9.0.0",
"targets": [
{ "expr": "histogram_quantile(0.50, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p50", "refId": "A" },
{ "expr": "histogram_quantile(0.95, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p95", "refId": "B" },
{ "expr": "histogram_quantile(0.99, sum(rate(ledger_write_latency_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p99", "refId": "C" }
],
"title": "Write Latency",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "ops" },
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"id": 12,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
"pluginVersion": "9.0.0",
"targets": [{ "expr": "rate(ledger_events_total{job=\"findings-ledger\"}[5m])", "legendFormat": "events/s", "refId": "A" }],
"title": "Event Write Rate",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
"id": 20,
"panels": [],
"title": "Merkle Anchoring",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "unit": "s" },
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"id": 21,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } },
"pluginVersion": "9.0.0",
"targets": [
{ "expr": "histogram_quantile(0.50, sum(rate(ledger_merkle_anchor_duration_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p50", "refId": "A" },
{ "expr": "histogram_quantile(0.95, sum(rate(ledger_merkle_anchor_duration_seconds_bucket{job=\"findings-ledger\"}[5m])) by (le))", "legendFormat": "p95", "refId": "B" }
],
"title": "Anchor Duration",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": { "color": { "mode": "thresholds" }, "unit": "short", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } },
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"id": 22,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"pluginVersion": "9.0.0",
"targets": [{ "expr": "ledger_merkle_anchors_total{job=\"findings-ledger\"}", "refId": "A" }],
"title": "Total Anchors",
"type": "stat"
}
],
"refresh": "30s",
"schemaVersion": 37,
"style": "dark",
"tags": ["stellaops", "findings-ledger"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "utc",
"title": "Findings Ledger",
"uid": "findings-ledger",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1 @@
# Container image tarballs populated at build time by offline-kit builder

View File

@@ -0,0 +1,106 @@
# Findings Ledger Offline Kit Manifest
# Version: 2025.11.0
# Generated: 2025-12-07
apiVersion: stellaops.io/v1
kind: OfflineKitManifest
metadata:
name: findings-ledger
version: "2025.11.0"
description: Findings Ledger service for event-sourced findings storage with Merkle anchoring
spec:
components:
- name: findings-ledger
type: service
image: stellaops/findings-ledger:2025.11.0
digest: "" # Populated at build time
- name: findings-ledger-migrations
type: job
image: stellaops/findings-ledger-migrations:2025.11.0
digest: "" # Populated at build time
dependencies:
- name: postgresql
version: ">=14.0"
type: database
required: true
- name: otel-collector
version: ">=0.80.0"
type: service
required: false
description: Optional for telemetry export
migrations:
- version: "001"
file: migrations/001_initial_schema.sql
checksum: "" # Populated at build time
- version: "002"
file: migrations/002_merkle_tables.sql
checksum: ""
- version: "003"
file: migrations/003_attachments.sql
checksum: ""
- version: "004"
file: migrations/004_projections.sql
checksum: ""
- version: "005"
file: migrations/005_airgap_imports.sql
checksum: ""
- version: "006"
file: migrations/006_evidence_snapshots.sql
checksum: ""
- version: "007"
file: migrations/007_timeline_events.sql
checksum: ""
- version: "008"
file: migrations/008_attestation_pointers.sql
checksum: ""
dashboards:
- name: findings-ledger
file: dashboards/findings-ledger.json
checksum: ""
alerts:
- name: findings-ledger-alerts
file: alerts/findings-ledger-alerts.yaml
checksum: ""
configuration:
required:
- key: LEDGER__DB__CONNECTIONSTRING
description: PostgreSQL connection string
secret: true
- key: LEDGER__ATTACHMENTS__ENCRYPTIONKEY
description: AES-256 encryption key for attachments (base64)
secret: true
optional:
- key: LEDGER__MERKLE__SIGNINGKEY
description: Signing key for Merkle root attestations
secret: true
- key: LEDGER__OBSERVABILITY__OTLPENDPOINT
description: OpenTelemetry collector endpoint
default: http://otel-collector:4317
- key: LEDGER__MERKLE__ANCHORINTERVAL
description: Merkle anchor interval (TimeSpan)
default: "00:05:00"
- key: LEDGER__AIRGAP__ADVISORYSTALETHRESHOLD
description: Advisory staleness threshold in seconds
default: "604800"
verification:
healthEndpoint: /health/ready
metricsEndpoint: /metrics
expectedMetrics:
- ledger_write_latency_seconds
- ledger_projection_lag_seconds
- ledger_merkle_anchor_duration_seconds
- ledger_events_total
checksums:
algorithm: sha256
manifest: "" # Populated at build time

View File

@@ -0,0 +1 @@
# Database migration SQL scripts copied from StellaOps.FindingsLedger.Migrations

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Import Findings Ledger container images into local Docker/containerd
# Usage: ./import-images.sh [registry-prefix]
#
# Example:
# ./import-images.sh # Loads as stellaops/*
# ./import-images.sh myregistry.local/ # Loads and tags as myregistry.local/stellaops/*
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IMAGES_DIR="${SCRIPT_DIR}/../images"
REGISTRY_PREFIX="${1:-}"
# Color output helpers
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# Detect container runtime
detect_runtime() {
if command -v docker &>/dev/null; then
echo "docker"
elif command -v nerdctl &>/dev/null; then
echo "nerdctl"
elif command -v podman &>/dev/null; then
echo "podman"
else
log_error "No container runtime found (docker, nerdctl, podman)"
exit 1
fi
}
RUNTIME=$(detect_runtime)
log_info "Using container runtime: $RUNTIME"
# Load images from tarballs
load_images() {
local count=0
for tarball in "${IMAGES_DIR}"/*.tar; do
if [[ -f "$tarball" ]]; then
log_info "Loading image from: $(basename "$tarball")"
if $RUNTIME load -i "$tarball"; then
((count++))
else
log_error "Failed to load: $tarball"
return 1
fi
fi
done
if [[ $count -eq 0 ]]; then
log_warn "No image tarballs found in $IMAGES_DIR"
log_warn "Run the offline kit builder first to populate images"
return 1
fi
log_info "Loaded $count image(s)"
}
# Re-tag images with custom registry prefix
retag_images() {
if [[ -z "$REGISTRY_PREFIX" ]]; then
log_info "No registry prefix specified, skipping re-tag"
return 0
fi
local images=(
"stellaops/findings-ledger"
"stellaops/findings-ledger-migrations"
)
for image in "${images[@]}"; do
# Get the loaded tag
local loaded_tag
loaded_tag=$($RUNTIME images --format '{{.Repository}}:{{.Tag}}' | grep "^${image}:" | head -1)
if [[ -n "$loaded_tag" ]]; then
local new_tag="${REGISTRY_PREFIX}${loaded_tag}"
log_info "Re-tagging: $loaded_tag -> $new_tag"
$RUNTIME tag "$loaded_tag" "$new_tag"
fi
done
}
# Verify loaded images
verify_images() {
log_info "Verifying loaded images..."
local images=(
"stellaops/findings-ledger"
"stellaops/findings-ledger-migrations"
)
local missing=0
for image in "${images[@]}"; do
if $RUNTIME images --format '{{.Repository}}' | grep -q "^${REGISTRY_PREFIX}${image}$"; then
log_info "${REGISTRY_PREFIX}${image}"
else
log_error "${REGISTRY_PREFIX}${image} not found"
((missing++))
fi
done
if [[ $missing -gt 0 ]]; then
log_error "$missing image(s) missing"
return 1
fi
log_info "All images verified"
}
main() {
log_info "Findings Ledger - Image Import"
log_info "=============================="
load_images
retag_images
verify_images
log_info "Image import complete"
}
main "$@"

View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# Run Findings Ledger database migrations
# Usage: ./run-migrations.sh [connection-string]
#
# Environment variables:
# LEDGER__DB__CONNECTIONSTRING - PostgreSQL connection string (if not provided as arg)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MIGRATIONS_DIR="${SCRIPT_DIR}/../migrations"
# Color output helpers
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# Get connection string
CONNECTION_STRING="${1:-${LEDGER__DB__CONNECTIONSTRING:-}}"
if [[ -z "$CONNECTION_STRING" ]]; then
log_error "Connection string required"
echo "Usage: $0 <connection-string>"
echo " or set LEDGER__DB__CONNECTIONSTRING environment variable"
exit 1
fi
# Detect container runtime
detect_runtime() {
if command -v docker &>/dev/null; then
echo "docker"
elif command -v nerdctl &>/dev/null; then
echo "nerdctl"
elif command -v podman &>/dev/null; then
echo "podman"
else
log_error "No container runtime found"
exit 1
fi
}
RUNTIME=$(detect_runtime)
# Run migrations via container
run_migrations_container() {
log_info "Running migrations via container..."
$RUNTIME run --rm \
-e "LEDGER__DB__CONNECTIONSTRING=${CONNECTION_STRING}" \
--network host \
stellaops/findings-ledger-migrations:2025.11.0 \
--connection "$CONNECTION_STRING"
}
# Alternative: Run migrations via psql (if dotnet not available)
run_migrations_psql() {
log_info "Running migrations via psql..."
if ! command -v psql &>/dev/null; then
log_error "psql not found and container runtime unavailable"
exit 1
fi
# Parse connection string for psql
# Expected format: Host=...;Port=...;Database=...;Username=...;Password=...
local host port database username password
host=$(echo "$CONNECTION_STRING" | grep -oP 'Host=\K[^;]+')
port=$(echo "$CONNECTION_STRING" | grep -oP 'Port=\K[^;]+' || echo "5432")
database=$(echo "$CONNECTION_STRING" | grep -oP 'Database=\K[^;]+')
username=$(echo "$CONNECTION_STRING" | grep -oP 'Username=\K[^;]+')
password=$(echo "$CONNECTION_STRING" | grep -oP 'Password=\K[^;]+')
export PGPASSWORD="$password"
for migration in "${MIGRATIONS_DIR}"/*.sql; do
if [[ -f "$migration" ]]; then
log_info "Applying: $(basename "$migration")"
psql -h "$host" -p "$port" -U "$username" -d "$database" -f "$migration"
fi
done
unset PGPASSWORD
}
verify_connection() {
log_info "Verifying database connection..."
# Try container-based verification
if $RUNTIME run --rm \
--network host \
postgres:14-alpine \
pg_isready -h "$(echo "$CONNECTION_STRING" | grep -oP 'Host=\K[^;]+')" \
-p "$(echo "$CONNECTION_STRING" | grep -oP 'Port=\K[^;]+' || echo 5432)" \
&>/dev/null; then
log_info "Database connection verified"
return 0
fi
log_warn "Could not verify database connection (may still work)"
return 0
}
main() {
log_info "Findings Ledger - Database Migrations"
log_info "======================================"
verify_connection
# Prefer container-based migrations
if $RUNTIME image inspect stellaops/findings-ledger-migrations:2025.11.0 &>/dev/null; then
run_migrations_container
else
log_warn "Migration image not found, falling back to psql"
run_migrations_psql
fi
log_info "Migrations complete"
}
main "$@"

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Verify Findings Ledger installation
# Usage: ./verify-install.sh [service-url]
set -euo pipefail
SERVICE_URL="${1:-http://localhost:8188}"
# Color output helpers
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_pass() { echo -e "${GREEN}${NC} $*"; }
log_fail() { echo -e "${RED}${NC} $*"; }
CHECKS_PASSED=0
CHECKS_FAILED=0
run_check() {
local name="$1"
local cmd="$2"
if eval "$cmd" &>/dev/null; then
log_pass "$name"
((CHECKS_PASSED++))
else
log_fail "$name"
((CHECKS_FAILED++))
fi
}
main() {
log_info "Findings Ledger - Installation Verification"
log_info "==========================================="
log_info "Service URL: $SERVICE_URL"
echo ""
log_info "Health Checks:"
run_check "Readiness endpoint" "curl -sf ${SERVICE_URL}/health/ready"
run_check "Liveness endpoint" "curl -sf ${SERVICE_URL}/health/live"
echo ""
log_info "Metrics Checks:"
run_check "Metrics endpoint available" "curl -sf ${SERVICE_URL}/metrics | head -1"
run_check "ledger_write_latency_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_write_latency_seconds"
run_check "ledger_projection_lag_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_projection_lag_seconds"
run_check "ledger_merkle_anchor_duration_seconds present" "curl -sf ${SERVICE_URL}/metrics | grep -q ledger_merkle_anchor_duration_seconds"
echo ""
log_info "API Checks:"
run_check "OpenAPI spec available" "curl -sf ${SERVICE_URL}/swagger/v1/swagger.json | head -1"
echo ""
log_info "========================================"
log_info "Results: ${CHECKS_PASSED} passed, ${CHECKS_FAILED} failed"
if [[ $CHECKS_FAILED -gt 0 ]]; then
log_error "Some checks failed. Review service logs for details."
exit 1
fi
log_info "All checks passed. Installation verified."
}
main "$@"

173
ops/wine-csp/Dockerfile Normal file
View File

@@ -0,0 +1,173 @@
# syntax=docker/dockerfile:1.7
# Wine CSP Service - GOST cryptographic operations via Wine-hosted CryptoPro CSP
#
# WARNING: For TEST VECTOR GENERATION ONLY - not for production signing
#
# Build:
# docker buildx build -f ops/wine-csp/Dockerfile -t wine-csp:latest .
#
# Run:
# docker run -p 5099:5099 -e WINE_CSP_MODE=limited wine-csp:latest
# ==============================================================================
# Stage 1: Build .NET application for Windows x64
# ==============================================================================
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0-preview-bookworm-slim
FROM ${SDK_IMAGE} AS build
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
DOTNET_NOLOGO=1 \
DOTNET_ROLL_FORWARD=LatestMajor \
SOURCE_DATE_EPOCH=1704067200
WORKDIR /src
# Copy solution files and NuGet configuration
COPY Directory.Build.props Directory.Build.rsp NuGet.config ./
# Copy local NuGet packages if available
COPY local-nugets/ ./local-nugets/
# Copy Wine CSP Service source
COPY src/__Tools/WineCspService/ ./src/__Tools/WineCspService/
# Copy GostCryptography fork dependency
COPY third_party/forks/AlexMAS.GostCryptography/ ./third_party/forks/AlexMAS.GostCryptography/
# Restore and publish for Windows x64 (runs under Wine)
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore src/__Tools/WineCspService/WineCspService.csproj && \
dotnet publish src/__Tools/WineCspService/WineCspService.csproj \
-c Release \
-r win-x64 \
--self-contained true \
-o /app/publish \
/p:PublishSingleFile=true \
/p:EnableCompressionInSingleFile=true \
/p:DebugType=none \
/p:DebugSymbols=false
# ==============================================================================
# Stage 2: Runtime with Wine and CryptoPro CSP support
# ==============================================================================
FROM ubuntu:22.04 AS runtime
# OCI Image Labels
LABEL org.opencontainers.image.title="StellaOps Wine CSP Service" \
org.opencontainers.image.description="GOST cryptographic test vector generation via Wine-hosted CryptoPro CSP" \
org.opencontainers.image.vendor="StellaOps" \
org.opencontainers.image.source="https://git.stella-ops.org/stellaops/router" \
com.stellaops.component="wine-csp" \
com.stellaops.security.production-signing="false" \
com.stellaops.security.test-vectors-only="true"
# Wine CSP service configuration
ARG WINE_CSP_PORT=5099
ARG APP_USER=winecsp
ARG APP_UID=10001
ARG APP_GID=10001
ENV DEBIAN_FRONTEND=noninteractive \
# Wine configuration
WINEDEBUG=-all \
WINEPREFIX=/home/${APP_USER}/.wine \
WINEARCH=win64 \
# Service configuration
WINE_CSP_PORT=${WINE_CSP_PORT} \
ASPNETCORE_URLS=http://+:${WINE_CSP_PORT} \
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
# CSP configuration
WINE_CSP_MODE=limited \
WINE_CSP_INSTALLER_PATH=/opt/cryptopro/csp-installer.msi \
WINE_CSP_LOG_LEVEL=Information \
# Display for Wine (headless)
DISPLAY=:99
# Install Wine and dependencies
# Using WineHQ stable repository for consistent Wine version
RUN set -eux; \
dpkg --add-architecture i386; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg2 \
software-properties-common \
wget \
xvfb \
cabextract \
p7zip-full \
procps; \
# Add WineHQ repository key
mkdir -pm755 /etc/apt/keyrings; \
wget -O /etc/apt/keyrings/winehq-archive.key \
https://dl.winehq.org/wine-builds/winehq.key; \
# Add WineHQ repository
wget -NP /etc/apt/sources.list.d/ \
https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources; \
apt-get update; \
# Install Wine stable
apt-get install -y --no-install-recommends \
winehq-stable; \
# Install winetricks for runtime dependencies
wget -O /usr/local/bin/winetricks \
https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks; \
chmod +x /usr/local/bin/winetricks; \
# Cleanup
apt-get clean; \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Create non-root user for Wine service
# Note: Wine requires writable home directory for prefix
RUN groupadd -r -g ${APP_GID} ${APP_USER} && \
useradd -r -u ${APP_UID} -g ${APP_GID} -m -d /home/${APP_USER} -s /bin/bash ${APP_USER} && \
mkdir -p /app /opt/cryptopro /var/log/wine-csp /var/run/wine-csp && \
chown -R ${APP_UID}:${APP_GID} /app /home/${APP_USER} /opt/cryptopro /var/log/wine-csp /var/run/wine-csp
WORKDIR /app
# Copy application from build stage
COPY --from=build --chown=${APP_UID}:${APP_GID} /app/publish/ ./
# Copy supporting scripts
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/healthcheck.sh /usr/local/bin/healthcheck.sh
COPY --chown=${APP_UID}:${APP_GID} ops/wine-csp/install-csp.sh /usr/local/bin/install-csp.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh /usr/local/bin/install-csp.sh
# Switch to non-root user for Wine prefix initialization
USER ${APP_UID}:${APP_GID}
# Initialize Wine prefix (creates .wine directory with Windows environment)
# This must run as the app user to set correct ownership
# Using xvfb-run for headless Wine initialization
RUN set -eux; \
# Start virtual framebuffer and initialize Wine
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \
wine64 wineboot --init; \
wineserver --wait; \
# Install Visual C++ 2019 runtime via winetricks (required for .NET)
xvfb-run --auto-servernum --server-args="-screen 0 1024x768x24" \
winetricks -q vcrun2019 || true; \
wineserver --wait; \
# Set Windows version to Windows 10 for compatibility
wine64 reg add "HKCU\\Software\\Wine\\Version" /v Windows /d "win10" /f || true; \
wineserver --wait; \
# Cleanup Wine temp files
rm -rf /home/${APP_USER}/.cache/winetricks /tmp/.X* /tmp/winetricks* || true
EXPOSE ${WINE_CSP_PORT}
# Health check using custom script that probes /health endpoint
# Extended start_period due to Wine initialization time
HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \
CMD /usr/local/bin/healthcheck.sh
# Volumes for persistence and CSP installer
# - Wine prefix: stores CSP installation, certificates, keys
# - CSP installer: mount customer-provided CryptoPro MSI here
# - Logs: service logs
VOLUME ["/home/${APP_USER}/.wine", "/opt/cryptopro", "/var/log/wine-csp"]
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["wine64", "/app/WineCspService.exe"]

227
ops/wine-csp/entrypoint.sh Normal file
View File

@@ -0,0 +1,227 @@
#!/bin/bash
# Wine CSP Service Entrypoint
#
# Initializes Wine environment and starts the WineCspService under Wine.
# For TEST VECTOR GENERATION ONLY - not for production signing.
set -euo pipefail
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
WINE_CSP_PORT="${WINE_CSP_PORT:-5099}"
WINE_CSP_MODE="${WINE_CSP_MODE:-limited}"
WINE_CSP_INSTALLER_PATH="${WINE_CSP_INSTALLER_PATH:-/opt/cryptopro/csp-installer.msi}"
WINE_CSP_LOG_LEVEL="${WINE_CSP_LOG_LEVEL:-Information}"
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
DISPLAY="${DISPLAY:-:99}"
# Marker files
CSP_INSTALLED_MARKER="${WINE_PREFIX}/.csp_installed"
WINE_INITIALIZED_MARKER="${WINE_PREFIX}/.wine_initialized"
# Log prefix for structured logging
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [entrypoint] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [entrypoint] [ERROR] $*" >&2
}
# ------------------------------------------------------------------------------
# Virtual Framebuffer Management
# ------------------------------------------------------------------------------
start_xvfb() {
if ! pgrep -x Xvfb > /dev/null; then
log "Starting Xvfb virtual framebuffer on display ${DISPLAY}"
Xvfb "${DISPLAY}" -screen 0 1024x768x24 &
sleep 2
fi
}
stop_xvfb() {
if pgrep -x Xvfb > /dev/null; then
log "Stopping Xvfb"
pkill -x Xvfb || true
fi
}
# ------------------------------------------------------------------------------
# Wine Initialization
# ------------------------------------------------------------------------------
initialize_wine() {
if [[ -f "${WINE_INITIALIZED_MARKER}" ]]; then
log "Wine prefix already initialized"
return 0
fi
log "Initializing Wine prefix at ${WINE_PREFIX}"
start_xvfb
# Initialize Wine prefix
wine64 wineboot --init 2>/dev/null || true
wineserver --wait
# Set Windows version for CryptoPro compatibility
wine64 reg add "HKCU\\Software\\Wine\\Version" /v Windows /d "win10" /f 2>/dev/null || true
wineserver --wait
# Create marker
touch "${WINE_INITIALIZED_MARKER}"
log "Wine prefix initialized successfully"
}
# ------------------------------------------------------------------------------
# CryptoPro CSP Installation
# ------------------------------------------------------------------------------
install_cryptopro() {
# Check if already installed
if [[ -f "${CSP_INSTALLED_MARKER}" ]]; then
log "CryptoPro CSP already installed"
return 0
fi
# Check if installer is available
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log "CryptoPro CSP installer not found at ${WINE_CSP_INSTALLER_PATH}"
log "Service will run in limited mode without CSP"
return 0
fi
log "Installing CryptoPro CSP from ${WINE_CSP_INSTALLER_PATH}"
start_xvfb
# Run the CSP installation script
if /usr/local/bin/install-csp.sh; then
touch "${CSP_INSTALLED_MARKER}"
log "CryptoPro CSP installed successfully"
else
log_error "CryptoPro CSP installation failed"
return 1
fi
}
# ------------------------------------------------------------------------------
# Service Configuration
# ------------------------------------------------------------------------------
configure_service() {
log "Configuring Wine CSP service"
log " Mode: ${WINE_CSP_MODE}"
log " Port: ${WINE_CSP_PORT}"
log " Log Level: ${WINE_CSP_LOG_LEVEL}"
# Configure Wine debug output based on log level
case "${WINE_CSP_LOG_LEVEL}" in
Trace|Debug)
export WINEDEBUG="warn+all"
;;
Information)
export WINEDEBUG="-all"
;;
Warning|Error|Critical)
export WINEDEBUG="-all"
;;
*)
export WINEDEBUG="-all"
;;
esac
# Set ASP.NET Core environment
export ASPNETCORE_URLS="http://+:${WINE_CSP_PORT}"
export ASPNETCORE_ENVIRONMENT="${ASPNETCORE_ENVIRONMENT:-Production}"
export Logging__LogLevel__Default="${WINE_CSP_LOG_LEVEL}"
# Check if CSP is available
if [[ -f "${CSP_INSTALLED_MARKER}" ]]; then
export WINE_CSP_CSP_AVAILABLE="true"
log "CryptoPro CSP is available"
else
export WINE_CSP_CSP_AVAILABLE="false"
log "Running without CryptoPro CSP (limited mode)"
fi
}
# ------------------------------------------------------------------------------
# Startup Validation
# ------------------------------------------------------------------------------
validate_environment() {
log "Validating environment"
# Check Wine is available
if ! command -v wine64 &> /dev/null; then
log_error "wine64 not found in PATH"
exit 1
fi
# Check application exists
if [[ ! -f "/app/WineCspService.exe" ]]; then
log_error "WineCspService.exe not found at /app/"
exit 1
fi
# Verify Wine prefix is writable
if [[ ! -w "${WINE_PREFIX}" ]]; then
log_error "Wine prefix ${WINE_PREFIX} is not writable"
exit 1
fi
log "Environment validation passed"
}
# ------------------------------------------------------------------------------
# Signal Handlers
# ------------------------------------------------------------------------------
cleanup() {
log "Received shutdown signal, cleaning up..."
# Stop Wine server gracefully
wineserver -k 15 2>/dev/null || true
sleep 2
wineserver -k 9 2>/dev/null || true
stop_xvfb
log "Cleanup complete"
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
# ------------------------------------------------------------------------------
# Main Entry Point
# ------------------------------------------------------------------------------
main() {
log "=========================================="
log "Wine CSP Service Entrypoint"
log "=========================================="
log "WARNING: For TEST VECTOR GENERATION ONLY"
log "=========================================="
validate_environment
initialize_wine
# Only attempt CSP installation in full mode
if [[ "${WINE_CSP_MODE}" == "full" ]]; then
install_cryptopro
fi
configure_service
# Start Xvfb for the main process
start_xvfb
log "Starting WineCspService..."
log "Listening on port ${WINE_CSP_PORT}"
# Execute the command passed to the container (or default)
if [[ $# -gt 0 ]]; then
exec "$@"
else
exec wine64 /app/WineCspService.exe
fi
}
main "$@"

View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Wine CSP Service Health Check
#
# Probes the /health endpoint to determine if the service is healthy.
# Returns 0 (healthy) or 1 (unhealthy).
set -euo pipefail
WINE_CSP_PORT="${WINE_CSP_PORT:-5099}"
HEALTH_ENDPOINT="http://127.0.0.1:${WINE_CSP_PORT}/health"
TIMEOUT_SECONDS=8
# Perform health check
response=$(wget -q -O - --timeout="${TIMEOUT_SECONDS}" "${HEALTH_ENDPOINT}" 2>/dev/null) || exit 1
# Verify response contains expected status
if echo "${response}" | grep -q '"status":"Healthy"'; then
exit 0
elif echo "${response}" | grep -q '"status":"Degraded"'; then
# Degraded is acceptable (e.g., CSP not installed but service running)
exit 0
else
exit 1
fi

215
ops/wine-csp/install-csp.sh Normal file
View File

@@ -0,0 +1,215 @@
#!/bin/bash
# CryptoPro CSP Installation Script for Wine
#
# Installs customer-provided CryptoPro CSP MSI under Wine environment.
# This script is called by entrypoint.sh when CSP installer is available.
#
# IMPORTANT: CryptoPro CSP is commercial software. The installer MSI must be
# provided by the customer with appropriate licensing. StellaOps does not
# distribute CryptoPro CSP.
set -euo pipefail
# ------------------------------------------------------------------------------
# Configuration
# ------------------------------------------------------------------------------
WINE_CSP_INSTALLER_PATH="${WINE_CSP_INSTALLER_PATH:-/opt/cryptopro/csp-installer.msi}"
WINE_PREFIX="${WINEPREFIX:-$HOME/.wine}"
DISPLAY="${DISPLAY:-:99}"
# Expected CSP installation paths (under Wine prefix)
CSP_PROGRAM_FILES="${WINE_PREFIX}/drive_c/Program Files/Crypto Pro"
CSP_MARKER="${WINE_PREFIX}/.csp_installed"
CSP_VERSION_FILE="${WINE_PREFIX}/.csp_version"
# Installation timeout (5 minutes)
INSTALL_TIMEOUT=300
# Log prefix
log() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [install-csp] $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] [install-csp] [ERROR] $*" >&2
}
# ------------------------------------------------------------------------------
# Pre-Installation Checks
# ------------------------------------------------------------------------------
check_prerequisites() {
log "Checking installation prerequisites"
# Check installer exists
if [[ ! -f "${WINE_CSP_INSTALLER_PATH}" ]]; then
log_error "CSP installer not found: ${WINE_CSP_INSTALLER_PATH}"
return 1
fi
# Verify file is an MSI
if ! file "${WINE_CSP_INSTALLER_PATH}" | grep -qi "microsoft installer"; then
log_error "File does not appear to be an MSI installer"
return 1
fi
# Check Wine is available
if ! command -v wine64 &> /dev/null; then
log_error "wine64 not found"
return 1
fi
# Check Wine prefix exists
if [[ ! -d "${WINE_PREFIX}" ]]; then
log_error "Wine prefix not initialized: ${WINE_PREFIX}"
return 1
fi
log "Prerequisites check passed"
return 0
}
# ------------------------------------------------------------------------------
# Installation
# ------------------------------------------------------------------------------
install_csp() {
log "Starting CryptoPro CSP installation"
log "Installer: ${WINE_CSP_INSTALLER_PATH}"
# Create installation log directory
local log_dir="${WINE_PREFIX}/csp_install_logs"
mkdir -p "${log_dir}"
local install_log="${log_dir}/install_$(date -u '+%Y%m%d_%H%M%S').log"
# Run MSI installer silently
# /qn = silent mode, /norestart = don't restart, /l*v = verbose logging
log "Running msiexec installer (this may take several minutes)..."
timeout "${INSTALL_TIMEOUT}" wine64 msiexec /i "${WINE_CSP_INSTALLER_PATH}" \
/qn /norestart /l*v "${install_log}" \
AGREETOLICENSE=Yes \
2>&1 | tee -a "${install_log}" || {
local exit_code=$?
log_error "MSI installation failed with exit code: ${exit_code}"
log_error "Check installation log: ${install_log}"
return 1
}
# Wait for Wine to finish
wineserver --wait
log "MSI installation completed"
return 0
}
# ------------------------------------------------------------------------------
# Post-Installation Verification
# ------------------------------------------------------------------------------
verify_installation() {
log "Verifying CryptoPro CSP installation"
# Check for CSP program files
if [[ -d "${CSP_PROGRAM_FILES}" ]]; then
log "Found CSP directory: ${CSP_PROGRAM_FILES}"
else
log_error "CSP program directory not found"
return 1
fi
# Check for key CSP DLLs
local csp_dll="${WINE_PREFIX}/drive_c/windows/system32/cpcspi.dll"
if [[ -f "${csp_dll}" ]]; then
log "Found CSP DLL: ${csp_dll}"
else
log "Warning: CSP DLL not found at expected location"
# This might be OK depending on CSP version
fi
# Try to query CSP registry entries
local csp_registry
csp_registry=$(wine64 reg query "HKLM\\SOFTWARE\\Crypto Pro" 2>/dev/null || true)
if [[ -n "${csp_registry}" ]]; then
log "CSP registry entries found"
else
log "Warning: CSP registry entries not found"
fi
# Extract version if possible
local version="unknown"
if [[ -f "${CSP_PROGRAM_FILES}/CSP/version.txt" ]]; then
version=$(cat "${CSP_PROGRAM_FILES}/CSP/version.txt" 2>/dev/null || echo "unknown")
fi
echo "${version}" > "${CSP_VERSION_FILE}"
log "CSP version: ${version}"
log "Installation verification completed"
return 0
}
# ------------------------------------------------------------------------------
# Cleanup on Failure
# ------------------------------------------------------------------------------
cleanup_failed_install() {
log "Cleaning up failed installation"
# Try to uninstall via msiexec
wine64 msiexec /x "${WINE_CSP_INSTALLER_PATH}" /qn 2>/dev/null || true
wineserver --wait
# Remove any partial installation directories
rm -rf "${CSP_PROGRAM_FILES}" 2>/dev/null || true
# Remove marker files
rm -f "${CSP_MARKER}" "${CSP_VERSION_FILE}" 2>/dev/null || true
log "Cleanup completed"
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
main() {
log "=========================================="
log "CryptoPro CSP Installation Script"
log "=========================================="
# Check if already installed
if [[ -f "${CSP_MARKER}" ]]; then
log "CryptoPro CSP is already installed"
if [[ -f "${CSP_VERSION_FILE}" ]]; then
log "Installed version: $(cat "${CSP_VERSION_FILE}")"
fi
return 0
fi
# Run prerequisite checks
if ! check_prerequisites; then
log_error "Prerequisites check failed"
return 1
fi
# Perform installation
if ! install_csp; then
log_error "Installation failed"
cleanup_failed_install
return 1
fi
# Verify installation
if ! verify_installation; then
log_error "Installation verification failed"
cleanup_failed_install
return 1
fi
# Create installation marker
touch "${CSP_MARKER}"
log "=========================================="
log "CryptoPro CSP installation successful"
log "=========================================="
return 0
}
main "$@"