consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -3,5 +3,5 @@
"advisoryai.validation.q_required": "q ist erforderlich.",
"advisoryai.validation.q_max_512": "q darf maximal 512 Zeichen lang sein.",
"advisoryai.validation.tenant_required": "Tenant-Kontext ist erforderlich."
"advisoryai.validation.tenant_required": "Mandantenkontext ist erforderlich."
}

File diff suppressed because it is too large Load Diff

View File

@@ -319,7 +319,7 @@
"sha256": "1be0ec1ce56cd616259095ccbfce106121c75e4d40621fbe2b1f022ff76072fe"
},
{
"path": "docs/modules/cli/guides/commands/orchestrator.md",
"path": "docs/modules/cli/guides/commands/jobengine.md",
"sha256": "5e74b92d1615f8300765ed156ed709c70645ad95f67b22f43bc47cc10589de30"
},
{
@@ -1583,7 +1583,7 @@
"sha256": "fa50d45dc2b02d2f89a12d801d38c83e3d89070caa318575123da54db9ea48c5"
},
{
"path": "docs/operations/orchestrator-runbook.md",
"path": "docs/operations/jobengine-runbook.md",
"sha256": "64af4dd5bda8eebb2e9323e2bf7ef8308b0dd2e2ba33a11bae20222f4945c247"
},
{
@@ -1715,23 +1715,23 @@
"sha256": "a1a31a4c8baf091f67e3a5b043118ee93a05c5314fb3eb1c3b6fd14e53c19d96"
},
{
"path": "docs/operations/runbooks/orchestrator-evidence-missing.md",
"path": "docs/operations/runbooks/jobengine-evidence-missing.md",
"sha256": "a180683a2de5a3fe60ae6477c1de7c5b36e37ad189c06373109c4afebe58b6da"
},
{
"path": "docs/operations/runbooks/orchestrator-gate-timeout.md",
"path": "docs/operations/runbooks/jobengine-gate-timeout.md",
"sha256": "3695ac55be0a165a4ca2052fa328f0dc312e20194cafeda8b3a9bda7ac599e76"
},
{
"path": "docs/operations/runbooks/orchestrator-promotion-stuck.md",
"path": "docs/operations/runbooks/jobengine-promotion-stuck.md",
"sha256": "bd5f464a941808d9bdb9a80d488c2885b3a9282fe4f7b05402e2b1766c22d276"
},
{
"path": "docs/operations/runbooks/orchestrator-quota-exceeded.md",
"path": "docs/operations/runbooks/jobengine-quota-exceeded.md",
"sha256": "1cfcaef596e6d75e54545c1e41bc310f5858f67f0a0800a0087ba92a7471d567"
},
{
"path": "docs/operations/runbooks/orchestrator-rollback-failed.md",
"path": "docs/operations/runbooks/jobengine-rollback-failed.md",
"sha256": "35abf5027af2e64060137fa9dbb23f500002c57ecea8ec762fa6fce1d4711073"
},
{

View File

@@ -48,8 +48,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\OpsMemory\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\..\Concelier\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.AdvisoryAI.Attestation\StellaOps.AdvisoryAI.Attestation.csproj" />
<!-- Evidence Packs (Sprint: SPRINT_20260109_011_005 Task: EVPK-006) -->
<ProjectReference Include="..\..\__Libraries\StellaOps.Evidence.Pack\StellaOps.Evidence.Pack.csproj" />

View File

@@ -48,7 +48,7 @@ internal sealed class PlatformCatalogIngestionAdapter : ISearchIngestionAdapter
EntityType: "pack",
Title: "Pack: Offline Kit",
Summary: "Offline kit export bundle",
Source: "orchestrator",
Source: "jobengine",
Route: "/packs/offline-kit"),
new PlatformCatalogEntry(
EntityId: "tenant-acme",

View File

@@ -16,8 +16,7 @@ using StellaOps.OpsMemory.WebService.Security;
var builder = WebApplication.CreateBuilder(args);
// Add PostgreSQL data source
var connectionString = builder.Configuration.GetConnectionString("OpsMemory")
?? "Host=localhost;Port=5432;Database=stellaops;Username=stellaops;Password=stellaops";
var connectionString = ResolveOpsMemoryConnectionString(builder);
builder.Services.AddSingleton<NpgsqlDataSource>(_ => NpgsqlDataSource.Create(connectionString));
// Add determinism abstractions (TimeProvider + IGuidProvider for endpoint parameter binding)
@@ -86,3 +85,26 @@ app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.LoadTranslationsAsync();
app.Run();
static string ResolveOpsMemoryConnectionString(WebApplicationBuilder builder)
{
// Explicit service connection has priority; shared default is the compose-compatible fallback.
var configuredConnectionString =
builder.Configuration.GetConnectionString("OpsMemory")
?? builder.Configuration["ConnectionStrings:OpsMemory"]
?? builder.Configuration.GetConnectionString("Default")
?? builder.Configuration["ConnectionStrings:Default"];
if (!string.IsNullOrWhiteSpace(configuredConnectionString))
{
return configuredConnectionString.Trim();
}
if (builder.Environment.IsDevelopment())
{
return "Host=localhost;Port=5432;Database=stellaops;Username=stellaops;Password=stellaops";
}
throw new InvalidOperationException(
"OpsMemory database connection string is required in non-development environments. Configure ConnectionStrings:OpsMemory or ConnectionStrings:Default.");
}

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| S312-OPSMEMORY-CONNECTION | DONE | Sprint `docs/implplan/SPRINT_20260305_312_DOCS_storage_policy_postgres_rustfs_alignment.md` TASK-312-007: aligned connection resolution with compose defaults (`ConnectionStrings:Default` fallback) and added fail-fast behavior for non-development when DB config is missing. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/OpsMemory/StellaOps.OpsMemory.WebService/StellaOps.OpsMemory.WebService.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

View File

@@ -14,6 +14,6 @@
<PackageReference Include="Npgsql" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Findings\StellaOps.Findings.Ledger\StellaOps.Findings.Ledger.csproj" />
<ProjectReference Include="..\..\..\Findings\StellaOps.Findings.Ledger\StellaOps.Findings.Ledger.csproj" />
</ItemGroup>
</Project>

View File

@@ -24,7 +24,7 @@
<ProjectReference Include="..\..\StellaOps.AdvisoryAI.Worker\StellaOps.AdvisoryAI.Worker.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\..\Plugin\StellaOps.Plugin.Testing\StellaOps.Plugin.Testing.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
</ItemGroup>

View File

@@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.OpsMemory\StellaOps.OpsMemory.csproj" />
</ItemGroup>
</Project>

View File

@@ -4,5 +4,6 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| Task ID | Status | Notes |
| --- | --- | --- |
| S312-OPSMEMORY-VERIFY | DONE | Sprint `docs/implplan/SPRINT_20260305_312_DOCS_storage_policy_postgres_rustfs_alignment.md` verification for TASK-312-007: `dotnet test src/AdvisoryAI/__Tests/StellaOps.OpsMemory.Tests/StellaOps.OpsMemory.Tests.csproj -v minimal` passed (50 tests). |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/OpsMemory/__Tests/StellaOps.OpsMemory.Tests/StellaOps.OpsMemory.Tests.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -18,6 +18,6 @@
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.HybridLogicalClock\StellaOps.HybridLogicalClock.csproj" />
<ProjectReference Include="..\..\..\Scheduler\__Libraries\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />
<ProjectReference Include="..\..\..\JobEngine\StellaOps.Scheduler.__Libraries\StellaOps.Scheduler.Models\StellaOps.Scheduler.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -17,9 +17,9 @@ servers:
- url: https://graph.stellaops.local
description: Example Graph endpoint
x-service: graph
- url: https://orchestrator.stellaops.local
- url: https://jobengine.stellaops.local
description: Example Orchestrator endpoint
x-service: orchestrator
x-service: jobengine
- url: https://policy.stellaops.local
description: Example Policy Engine endpoint
x-service: policy
@@ -696,13 +696,13 @@ paths:
traceId: "5"
x-service: graph
x-original-path: /healthz
/orchestrator/health:
/jobengine/health:
get:
tags:
- Health
summary: Liveness probe
description: Returns OK when Orchestrator is reachable.
operationId: orchestratorHealth
operationId: jobengineHealth
responses:
"200":
description: Service is up
@@ -712,7 +712,7 @@ paths:
ok:
value:
status: ok
service: orchestrator
service: jobengine
timestamp: 2025-11-18T00:00:00Z
"503":
description: Service unhealthy or dependencies unavailable.
@@ -722,18 +722,18 @@ paths:
unhealthy:
value:
status: degraded
service: orchestrator
service: jobengine
reason: scheduler queue unreachable
timestamp: 2025-11-18T00:00:00Z
x-service: orchestrator
x-service: jobengine
x-original-path: /health
/orchestrator/healthz:
/jobengine/healthz:
get:
summary: Service health
tags:
- Meta
description: Readiness probe for orchestrator dependencies.
operationId: orchestratorHealthz
description: Readiness probe for jobengine dependencies.
operationId: jobengineHealthz
responses:
"200":
description: Service healthy
@@ -746,7 +746,7 @@ paths:
summary: Healthy response
value:
status: ok
service: orchestrator
service: jobengine
"503":
description: Service unavailable
content:
@@ -760,14 +760,14 @@ paths:
code: service_unavailable
message: outbound queue lag exceeds threshold
traceId: "1"
x-service: orchestrator
x-service: jobengine
x-original-path: /healthz
/orchestrator/jobs:
/jobengine/jobs:
get:
tags:
- Jobs
summary: List jobs
operationId: orchestratorListJobs
operationId: jobengineListJobs
description: Returns jobs for the tenant with optional status filter.
parameters:
- in: query
@@ -791,7 +791,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/orchestrator.JobSummary"
$ref: "#/components/schemas/jobengine.JobSummary"
examples:
default:
summary: Mixed queues
@@ -833,13 +833,13 @@ paths:
code: orch.invalid_request
message: status must be one of queued,running,failed,completed.
traceId: 01JF04ERR1
x-service: orchestrator
x-service: jobengine
x-original-path: /jobs
post:
tags:
- Jobs
summary: Submit a job to the orchestrator queue
operationId: orchestratorSubmitJob
summary: Submit a job to the jobengine queue
operationId: jobengineSubmitJob
description: Enqueue a job for asynchronous execution.
parameters:
- in: header
@@ -854,7 +854,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobCreateRequest"
$ref: "#/components/schemas/jobengine.JobCreateRequest"
examples:
scanJob:
summary: Submit scan job
@@ -874,7 +874,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobCreateResponse"
$ref: "#/components/schemas/jobengine.JobCreateResponse"
examples:
accepted:
summary: Job enqueued
@@ -896,14 +896,14 @@ paths:
code: orch.invalid_request
message: jobType is required.
traceId: 01JF04ERR1
x-service: orchestrator
x-service: jobengine
x-original-path: /jobs
/orchestrator/jobs/{jobId}:
/jobengine/jobs/{jobId}:
get:
tags:
- Jobs
summary: Get job status
operationId: orchestratorGetJob
operationId: jobengineGetJob
description: Fetch the current status of a job by id.
parameters:
- name: jobId
@@ -917,7 +917,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobSummary"
$ref: "#/components/schemas/jobengine.JobSummary"
examples:
sample:
value:
@@ -930,8 +930,8 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.ErrorEnvelope"
x-service: orchestrator
$ref: "#/components/schemas/jobengine.ErrorEnvelope"
x-service: jobengine
x-original-path: /jobs/{jobId}
/policy/evaluate:
post:
@@ -1670,7 +1670,7 @@ components:
required:
- status
- service
orchestrator.ErrorEnvelope:
jobengine.ErrorEnvelope:
type: object
properties:
code:
@@ -1682,7 +1682,7 @@ components:
required:
- code
- message
orchestrator.JobCreateRequest:
jobengine.JobCreateRequest:
type: object
required:
- kind
@@ -1702,7 +1702,7 @@ components:
- high
tenant:
type: string
orchestrator.JobCreateResponse:
jobengine.JobCreateResponse:
type: object
required:
- jobId
@@ -1717,7 +1717,7 @@ components:
enqueuedAt:
type: string
format: date-time
orchestrator.JobSummary:
jobengine.JobSummary:
type: object
required:
- jobId

View File

@@ -15,7 +15,7 @@ tags:
- name: Jobs
description: Job submission and status APIs
servers:
- url: https://orchestrator.stellaops.local
- url: https://jobengine.stellaops.local
description: Example Orchestrator endpoint
paths:
/health:
@@ -24,7 +24,7 @@ paths:
- Health
summary: Liveness probe
description: Returns OK when Orchestrator is reachable.
operationId: orchestratorHealth
operationId: jobengineHealth
responses:
'200':
description: Service is up
@@ -34,7 +34,7 @@ paths:
ok:
value:
status: ok
service: orchestrator
service: jobengine
timestamp: '2025-11-18T00:00:00Z'
'503':
description: Service unhealthy or dependencies unavailable.
@@ -44,7 +44,7 @@ paths:
unhealthy:
value:
status: degraded
service: orchestrator
service: jobengine
reason: scheduler queue unreachable
timestamp: '2025-11-18T00:00:00Z'
/healthz:
@@ -52,8 +52,8 @@ paths:
summary: Service health
tags:
- Meta
description: Readiness probe for orchestrator dependencies.
operationId: orchestratorHealthz
description: Readiness probe for jobengine dependencies.
operationId: jobengineHealthz
responses:
'200':
description: Service healthy
@@ -66,7 +66,7 @@ paths:
summary: Healthy response
value:
status: ok
service: orchestrator
service: jobengine
'503':
description: Service unavailable
content:
@@ -84,8 +84,8 @@ paths:
post:
tags:
- Jobs
summary: Submit a job to the orchestrator queue
operationId: orchestratorSubmitJob
summary: Submit a job to the jobengine queue
operationId: jobengineSubmitJob
description: Enqueue a job for asynchronous execution.
parameters:
- in: header
@@ -146,7 +146,7 @@ paths:
tags:
- Jobs
summary: List jobs
operationId: orchestratorListJobs
operationId: jobengineListJobs
description: Returns jobs for the tenant with optional status filter.
parameters:
- in: query
@@ -217,7 +217,7 @@ paths:
tags:
- Jobs
summary: Get job status
operationId: orchestratorGetJob
operationId: jobengineGetJob
description: Fetch the current status of a job by id.
parameters:
- name: jobId

View File

@@ -17,9 +17,9 @@ servers:
- url: https://graph.stellaops.local
description: Example Graph endpoint
x-service: graph
- url: https://orchestrator.stellaops.local
- url: https://jobengine.stellaops.local
description: Example Orchestrator endpoint
x-service: orchestrator
x-service: jobengine
- url: https://policy.stellaops.local
description: Example Policy Engine endpoint
x-service: policy
@@ -711,13 +711,13 @@ paths:
traceId: "5"
x-service: graph
x-original-path: /healthz
/orchestrator/health:
/jobengine/health:
get:
tags:
- Health
summary: Liveness probe
description: Returns OK when Orchestrator is reachable.
operationId: orchestratorHealth
operationId: jobengineHealth
responses:
"200":
description: Service is up
@@ -727,7 +727,7 @@ paths:
ok:
value:
status: ok
service: orchestrator
service: jobengine
timestamp: 2025-11-18T00:00:00Z
"503":
description: Service unhealthy or dependencies unavailable.
@@ -737,18 +737,18 @@ paths:
unhealthy:
value:
status: degraded
service: orchestrator
service: jobengine
reason: scheduler queue unreachable
timestamp: 2025-11-18T00:00:00Z
x-service: orchestrator
x-service: jobengine
x-original-path: /health
/orchestrator/healthz:
/jobengine/healthz:
get:
summary: Service health
tags:
- Meta
description: Readiness probe for orchestrator dependencies.
operationId: orchestratorHealthz
description: Readiness probe for jobengine dependencies.
operationId: jobengineHealthz
responses:
"200":
description: Service healthy
@@ -761,7 +761,7 @@ paths:
summary: Healthy response
value:
status: ok
service: orchestrator
service: jobengine
"503":
description: Service unavailable
content:
@@ -775,14 +775,14 @@ paths:
code: service_unavailable
message: outbound queue lag exceeds threshold
traceId: "1"
x-service: orchestrator
x-service: jobengine
x-original-path: /healthz
/orchestrator/jobs:
/jobengine/jobs:
get:
tags:
- Jobs
summary: List jobs
operationId: orchestratorListJobs
operationId: jobengineListJobs
description: Returns jobs for the tenant with optional status filter.
parameters:
- in: query
@@ -806,7 +806,7 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/orchestrator.JobSummary"
$ref: "#/components/schemas/jobengine.JobSummary"
examples:
default:
summary: Mixed queues
@@ -848,13 +848,13 @@ paths:
code: orch.invalid_request
message: status must be one of queued,running,failed,completed.
traceId: 01JF04ERR1
x-service: orchestrator
x-service: jobengine
x-original-path: /jobs
post:
tags:
- Jobs
summary: Submit a job to the orchestrator queue
operationId: orchestratorSubmitJob
summary: Submit a job to the jobengine queue
operationId: jobengineSubmitJob
description: Enqueue a job for asynchronous execution.
parameters:
- in: header
@@ -869,7 +869,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobCreateRequest"
$ref: "#/components/schemas/jobengine.JobCreateRequest"
examples:
scanJob:
summary: Submit scan job
@@ -889,7 +889,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobCreateResponse"
$ref: "#/components/schemas/jobengine.JobCreateResponse"
examples:
accepted:
summary: Job enqueued
@@ -911,14 +911,14 @@ paths:
code: orch.invalid_request
message: jobType is required.
traceId: 01JF04ERR1
x-service: orchestrator
x-service: jobengine
x-original-path: /jobs
/orchestrator/jobs/{jobId}:
/jobengine/jobs/{jobId}:
get:
tags:
- Jobs
summary: Get job status
operationId: orchestratorGetJob
operationId: jobengineGetJob
description: Fetch the current status of a job by id.
parameters:
- name: jobId
@@ -932,7 +932,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.JobSummary"
$ref: "#/components/schemas/jobengine.JobSummary"
examples:
sample:
value:
@@ -945,8 +945,8 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/orchestrator.ErrorEnvelope"
x-service: orchestrator
$ref: "#/components/schemas/jobengine.ErrorEnvelope"
x-service: jobengine
x-original-path: /jobs/{jobId}
/policy/evaluate:
post:
@@ -2252,7 +2252,7 @@ components:
required:
- status
- service
orchestrator.ErrorEnvelope:
jobengine.ErrorEnvelope:
type: object
properties:
code:
@@ -2264,7 +2264,7 @@ components:
required:
- code
- message
orchestrator.JobCreateRequest:
jobengine.JobCreateRequest:
type: object
required:
- kind
@@ -2284,7 +2284,7 @@ components:
- high
tenant:
type: string
orchestrator.JobCreateResponse:
jobengine.JobCreateResponse:
type: object
required:
- jobId
@@ -2299,7 +2299,7 @@ components:
enqueuedAt:
type: string
format: date-time
orchestrator.JobSummary:
jobengine.JobSummary:
type: object
required:
- jobId

View File

@@ -16,11 +16,26 @@ Manage the attestation and proof chain infrastructure for StellaOps:
- Keep proof chain storage schema current with migrations.
## Key Components
### Attestor (transparency logging and verification)
- **StellaOps.Attestor**: Main attestation service and REST API endpoints
- **StellaOps.Attestor.Envelope**: DSSE envelope handling and serialization
- **StellaOps.Attestor.Types**: Core attestation models and schemas
- **StellaOps.Attestor.Verify**: Verification engine for signatures and Rekor proofs
- **__Libraries**: Shared attestation utilities and storage abstractions
- **__Libraries/StellaOps.Attestor.***: Shared attestation utilities and storage abstractions
### Signer (cryptographic signing -- trust domain co-located, Sprint 204)
- **StellaOps.Signer/StellaOps.Signer.Core**: Signing pipeline, predicate types, DSSE statement builder
- **StellaOps.Signer/StellaOps.Signer.Infrastructure**: Redis/cache/HTTP infrastructure for signing
- **StellaOps.Signer/StellaOps.Signer.WebService**: REST API (`/api/v1/signer/sign/dsse`)
- **__Libraries/StellaOps.Signer.KeyManagement**: Key rotation, trust anchors, HSM/KMS bindings (separate DB schema)
- **__Libraries/StellaOps.Signer.Keyless**: Fulcio/Sigstore keyless signing support
### Provenance (attestation library -- trust domain co-located, Sprint 204)
- **StellaOps.Provenance.Attestation**: SLSA/DSSE attestation generation library
- **StellaOps.Provenance.Attestation.Tool**: Forensic verification CLI tool
### Tests
- **__Tests**: Integration tests with Testcontainers for PostgreSQL
## Required Reading

View File

@@ -4,6 +4,9 @@
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Security;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
@@ -133,24 +136,21 @@ public sealed class DsseVerifier : IDsseVerifier
try
{
var signatureBytes = Convert.FromBase64String(sig.Sig);
if (VerifySignature(pae, signatureBytes, publicKeyPem))
var verification = VerifySignature(pae, signatureBytes, publicKeyPem);
if (verification.IsValid)
{
verifiedKeyIds.Add(sig.KeyId ?? "unknown");
_logger.LogDebug("DSSE signature verified for keyId: {KeyId}", sig.KeyId ?? "unknown");
}
else
{
issues.Add($"signature_invalid_{sig.KeyId ?? "unknown"}");
issues.Add($"signature_invalid_{sig.KeyId ?? "unknown"}:{verification.ReasonCode}");
}
}
catch (FormatException)
{
issues.Add($"signature_invalid_base64_{sig.KeyId ?? "unknown"}");
}
catch (CryptographicException ex)
{
issues.Add($"signature_crypto_error_{sig.KeyId ?? "unknown"}: {ex.Message}");
}
}
// Compute payload hash for result
@@ -236,49 +236,164 @@ public sealed class DsseVerifier : IDsseVerifier
/// <summary>
/// Verifies a signature against PAE using the provided public key.
/// Supports ECDSA P-256 and RSA keys.
/// Supports ECDSA, RSA, and Ed25519 keys.
/// </summary>
private bool VerifySignature(byte[] pae, byte[] signature, string publicKeyPem)
private SignatureVerificationResult VerifySignature(byte[] pae, byte[] signature, string publicKeyPem)
{
if (!TryExtractPublicKeyDer(publicKeyPem, out var publicKeyDer))
{
return SignatureVerificationResult.Invalid("invalid_public_key_material");
}
if (TryVerifyWithEcdsa(pae, signature, publicKeyDer, out var ecdsaResult))
{
return ecdsaResult;
}
if (TryVerifyWithRsa(pae, signature, publicKeyDer, out var rsaResult))
{
return rsaResult;
}
if (TryVerifyWithEd25519(pae, signature, publicKeyDer, out var ed25519Result))
{
return ed25519Result;
}
return SignatureVerificationResult.Invalid("unsupported_public_key_type");
}
private static bool TryVerifyWithEcdsa(
byte[] pae,
byte[] signature,
byte[] publicKeyDer,
out SignatureVerificationResult result)
{
// Try ECDSA first (most common for Sigstore/Fulcio)
try
{
using var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(publicKeyPem);
return ecdsa.VerifyData(pae, signature, HashAlgorithmName.SHA256);
ecdsa.ImportSubjectPublicKeyInfo(publicKeyDer, out _);
var isValid = ecdsa.VerifyData(pae, signature, HashAlgorithmName.SHA256);
result = isValid
? SignatureVerificationResult.Valid
: SignatureVerificationResult.Invalid("signature_mismatch");
return true;
}
catch (CryptographicException)
{
// Not an ECDSA key, try RSA
result = SignatureVerificationResult.NotApplicable;
return false;
}
}
// Try RSA
private static bool TryVerifyWithRsa(
byte[] pae,
byte[] signature,
byte[] publicKeyDer,
out SignatureVerificationResult result)
{
try
{
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
return rsa.VerifyData(pae, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
rsa.ImportSubjectPublicKeyInfo(publicKeyDer, out _);
var isValid = rsa.VerifyData(pae, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
result = isValid
? SignatureVerificationResult.Valid
: SignatureVerificationResult.Invalid("signature_mismatch");
return true;
}
catch (CryptographicException)
{
// Not an RSA key either
}
// Try Ed25519 if available (.NET 9+)
try
{
// Ed25519 support via System.Security.Cryptography
// Note: Ed25519 verification requires different handling
// For now, we log and return false - can be extended later
_logger.LogDebug("Ed25519 signature verification not yet implemented");
result = SignatureVerificationResult.NotApplicable;
return false;
}
catch
}
private static bool TryVerifyWithEd25519(
byte[] pae,
byte[] signature,
byte[] publicKeyDer,
out SignatureVerificationResult result)
{
try
{
// Ed25519 not available
var key = PublicKeyFactory.CreateKey(publicKeyDer);
if (key is not Ed25519PublicKeyParameters ed25519PublicKey)
{
result = SignatureVerificationResult.NotApplicable;
return false;
}
var verifier = new Ed25519Signer();
verifier.Init(false, ed25519PublicKey);
verifier.BlockUpdate(pae, 0, pae.Length);
var isValid = verifier.VerifySignature(signature);
result = isValid
? SignatureVerificationResult.Valid
: SignatureVerificationResult.Invalid("signature_mismatch");
return true;
}
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
{
result = SignatureVerificationResult.Invalid("invalid_public_key_material");
return true;
}
}
private static bool TryExtractPublicKeyDer(string publicKeyPem, out byte[] publicKeyDer)
{
publicKeyDer = Array.Empty<byte>();
if (string.IsNullOrWhiteSpace(publicKeyPem))
{
return false;
}
return false;
var beginMarker = "-----BEGIN PUBLIC KEY-----";
var endMarker = "-----END PUBLIC KEY-----";
var beginIndex = publicKeyPem.IndexOf(beginMarker, StringComparison.Ordinal);
var endIndex = publicKeyPem.IndexOf(endMarker, StringComparison.Ordinal);
if (beginIndex < 0 || endIndex <= beginIndex)
{
return false;
}
var bodyStart = beginIndex + beginMarker.Length;
var body = publicKeyPem[bodyStart..endIndex];
var normalized = new string(body.Where(static ch => !char.IsWhiteSpace(ch)).ToArray());
if (string.IsNullOrWhiteSpace(normalized))
{
return false;
}
try
{
publicKeyDer = Convert.FromBase64String(normalized);
return publicKeyDer.Length > 0;
}
catch (FormatException)
{
return false;
}
}
private readonly struct SignatureVerificationResult
{
public static SignatureVerificationResult Valid => new(true, "none");
public static SignatureVerificationResult NotApplicable => new(false, "not_applicable");
public bool IsValid { get; }
public string ReasonCode { get; }
private SignatureVerificationResult(bool isValid, string reasonCode)
{
IsValid = isValid;
ReasonCode = reasonCode;
}
public static SignatureVerificationResult Invalid(string reasonCode) => new(false, reasonCode);
}
/// <summary>

View File

@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| ATTESTOR-225-001 | DOING | Sprint 225: implement Ed25519 DSSE verification with deterministic failure reasons and vectors. |
| AUDIT-0043-M | DONE | Revalidated maintainability for StellaOps.Attestation (2026-01-06). |
| AUDIT-0043-T | DONE | Revalidated test coverage for StellaOps.Attestation (2026-01-06). |
| AUDIT-0043-A | TODO | Open findings from revalidation (canonical JSON for DSSE payloads). |

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />
<ProjectReference Include="..\..\..\Signer\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
<ProjectReference Include="..\..\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,6 +5,9 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| ATTESTOR-225-002 | DOING | Sprint 225 endpoint tests for trusted/revoked/unknown key scenarios. |
| ATTESTOR-225-003 | DOING | Sprint 225 tenant isolation and claim-derived tenant tests. |
| ATTESTOR-225-004 | DOING | Sprint 225 verdict-by-hash retrieval tests with authorization checks. |
| AUDIT-0066-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0066-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0066-A | DONE | Waived (test project; revalidated 2026-01-06). |

View File

@@ -11,6 +11,7 @@ using OpenTelemetry.Trace;
using Serilog;
using Serilog.Context;
using Serilog.Events;
using StellaOps.Attestation;
using StellaOps.Attestor.Core.Bulk;
using StellaOps.Attestor.Core.Observability;
using StellaOps.Attestor.Core.Options;
@@ -130,6 +131,8 @@ internal static class AttestorWebServiceComposition
builder.Services.AddOptions<AttestorWebServiceFeatures>()
.Bind(builder.Configuration.GetSection($"{configurationSection}:features"))
.ValidateOnStart();
builder.Services.AddOptions<VerdictAuthorityRosterOptions>()
.Bind(builder.Configuration.GetSection($"{configurationSection}:verdictTrust"));
var featureOptions = builder.Configuration.GetSection($"{configurationSection}:features")
.Get<AttestorWebServiceFeatures>() ?? new AttestorWebServiceFeatures();
@@ -141,6 +144,7 @@ internal static class AttestorWebServiceComposition
manager.FeatureProviders.Add(new AttestorWebServiceControllerFeatureProvider(featureOptions));
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSingleton<IDsseVerifier, DsseVerifier>();
builder.Services.AddAttestorInfrastructure();
builder.Services.AddProofChainServices();

View File

@@ -98,3 +98,27 @@ public sealed class VerdictAttestationResponseDto
[JsonPropertyName("createdAt")]
public string CreatedAt { get; init; } = string.Empty;
}
/// <summary>
/// Response for verdict lookup by deterministic hash.
/// </summary>
public sealed class VerdictLookupResponseDto
{
[JsonPropertyName("verdictId")]
public string VerdictId { get; init; } = string.Empty;
[JsonPropertyName("attestationUri")]
public string AttestationUri { get; init; } = string.Empty;
[JsonPropertyName("envelope")]
public string Envelope { get; init; } = string.Empty;
[JsonPropertyName("keyId")]
public string KeyId { get; init; } = string.Empty;
[JsonPropertyName("createdAt")]
public string CreatedAt { get; init; } = string.Empty;
[JsonPropertyName("tenantId")]
public string TenantId { get; init; } = string.Empty;
}

View File

@@ -5,13 +5,17 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Attestation;
using StellaOps.Attestor.Core.Signing;
using StellaOps.Attestor.Core.Submission;
using StellaOps.Attestor.WebService.Contracts;
using StellaOps.Attestor.WebService.Options;
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
@@ -31,21 +35,28 @@ public class VerdictController : ControllerBase
{
private readonly IAttestationSigningService _signingService;
private readonly ILogger<VerdictController> _logger;
private readonly IDsseVerifier _dsseVerifier;
private readonly IHttpClientFactory? _httpClientFactory;
private readonly AttestorWebServiceFeatures _features;
private readonly VerdictAuthorityRosterOptions _verdictRosterOptions;
private readonly TimeProvider _timeProvider;
private static readonly ConcurrentDictionary<string, CachedVerdictRecord> VerdictCache = new(StringComparer.Ordinal);
public VerdictController(
IAttestationSigningService signingService,
ILogger<VerdictController> logger,
IDsseVerifier dsseVerifier,
IHttpClientFactory? httpClientFactory = null,
IOptions<AttestorWebServiceFeatures>? features = null,
IOptions<VerdictAuthorityRosterOptions>? verdictRosterOptions = null,
TimeProvider? timeProvider = null)
{
_signingService = signingService ?? throw new ArgumentNullException(nameof(signingService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
_httpClientFactory = httpClientFactory;
_features = features?.Value ?? new AttestorWebServiceFeatures();
_verdictRosterOptions = verdictRosterOptions?.Value ?? new VerdictAuthorityRosterOptions();
_timeProvider = timeProvider ?? TimeProvider.System;
}
@@ -75,6 +86,14 @@ public class VerdictController : ControllerBase
"Creating verdict attestation for subject {SubjectName}",
request.Subject.Name);
var tenantResolutionResult = ResolveTenantContext(User, Request.Headers);
if (tenantResolutionResult.Error is not null)
{
return tenantResolutionResult.Error;
}
var tenantId = tenantResolutionResult.TenantId!;
// Validate request
if (string.IsNullOrWhiteSpace(request.PredicateType))
{
@@ -114,9 +133,17 @@ public class VerdictController : ControllerBase
var predicateBase64 = Convert.ToBase64String(predicateBytes);
// Create signing request
var requestedKeyId = string.IsNullOrWhiteSpace(request.KeyId) ? "default" : request.KeyId.Trim();
var rosterResolution = ResolveRosterEntry(requestedKeyId);
if (rosterResolution.Error is not null)
{
return rosterResolution.Error;
}
var rosterEntry = rosterResolution.Entry!;
var signingRequest = new AttestationSignRequest
{
KeyId = request.KeyId ?? "default",
KeyId = requestedKeyId,
PayloadType = request.PredicateType,
PayloadBase64 = predicateBase64
};
@@ -127,7 +154,7 @@ public class VerdictController : ControllerBase
CallerSubject = "system",
CallerAudience = "policy-engine",
CallerClientId = "policy-engine-verdict-attestor",
CallerTenant = "default" // TODO: Extract from auth context
CallerTenant = tenantId
};
// Sign the predicate
@@ -137,12 +164,37 @@ public class VerdictController : ControllerBase
var envelope = signResult.Bundle.Dsse;
var envelopeJson = SerializeEnvelope(envelope, signResult.KeyId);
if (!string.Equals(signResult.KeyId, rosterEntry.KeyId, StringComparison.Ordinal))
{
return StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Signing key is not trusted by roster.",
detail: $"Signed key '{signResult.KeyId}' does not match roster key '{rosterEntry.KeyId}'.",
status: StatusCodes.Status403Forbidden,
code: "authority_key_mismatch"));
}
var signatureVerification = await _dsseVerifier.VerifyAsync(envelopeJson, rosterEntry.PublicKeyPem, ct).ConfigureAwait(false);
if (!signatureVerification.IsValid)
{
return StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Verdict signature is untrusted.",
detail: "Signed verdict DSSE envelope failed authority roster verification.",
status: StatusCodes.Status403Forbidden,
code: "authority_signature_untrusted",
issues: signatureVerification.Issues.ToArray()));
}
// Rekor log index (not implemented in minimal handler)
long? rekorLogIndex = null;
// Store in Evidence Locker (via HTTP call)
await StoreVerdictInEvidenceLockerAsync(
verdictId,
tenantId,
request.Subject.Name,
envelopeJson,
signResult,
@@ -158,15 +210,13 @@ public class VerdictController : ControllerBase
KeyId = signResult.KeyId ?? request.KeyId ?? "default",
CreatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
};
VerdictCache[verdictId] = CachedVerdictRecord.From(response, tenantId);
_logger.LogInformation(
"Verdict attestation created successfully: {VerdictId}",
verdictId);
return CreatedAtRoute(
routeName: null, // No route name needed for external link
routeValues: null,
value: response);
return Created(attestationUri, response);
}
catch (Exception ex)
{
@@ -186,6 +236,60 @@ public class VerdictController : ControllerBase
}
}
/// <summary>
/// Retrieves a verdict attestation by deterministic verdict hash.
/// </summary>
[HttpGet("~/api/v1/verdicts/{verdictId}")]
[Authorize("attestor:read")]
[EnableRateLimiting("attestor-reads")]
[ProducesResponseType(typeof(VerdictLookupResponseDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<VerdictLookupResponseDto>> GetVerdictByHashAsync(
[FromRoute] string verdictId,
CancellationToken ct = default)
{
if (!_features.VerdictsEnabled)
{
return NotImplementedResult();
}
if (string.IsNullOrWhiteSpace(verdictId))
{
return BadRequest(CreateProblem(
title: "Invalid verdict identifier.",
detail: "verdictId is required.",
status: StatusCodes.Status400BadRequest,
code: "invalid_verdict_id"));
}
var tenantResolutionResult = ResolveTenantContext(User, Request.Headers);
if (tenantResolutionResult.Error is not null)
{
return tenantResolutionResult.Error;
}
var tenantId = tenantResolutionResult.TenantId!;
if (VerdictCache.TryGetValue(verdictId, out var cached) &&
string.Equals(cached.TenantId, tenantId, StringComparison.Ordinal))
{
return Ok(cached.ToLookupResponse(verdictId));
}
var lockerResult = await FetchVerdictFromEvidenceLockerAsync(verdictId, tenantId, ct).ConfigureAwait(false);
if (lockerResult is not null)
{
VerdictCache[verdictId] = CachedVerdictRecord.From(lockerResult);
return Ok(lockerResult);
}
return NotFound(CreateProblem(
title: "Verdict not found.",
detail: $"No verdict exists for hash '{verdictId}' in tenant '{tenantId}'.",
status: StatusCodes.Status404NotFound,
code: "verdict_not_found"));
}
/// <summary>
/// Computes a deterministic verdict ID from predicate content.
/// </summary>
@@ -227,6 +331,7 @@ public class VerdictController : ControllerBase
/// </summary>
private async Task StoreVerdictInEvidenceLockerAsync(
string verdictId,
string tenantId,
string findingId,
string envelopeJson,
AttestationSignResult signResult,
@@ -268,7 +373,7 @@ public class VerdictController : ControllerBase
var storeRequest = new
{
verdict_id = verdictId,
tenant_id = "default", // TODO: Extract from auth context (requires CallerTenant from SubmissionContext)
tenant_id = tenantId,
policy_run_id = policyRunId,
policy_id = policyId,
policy_version = policyVersion,
@@ -310,6 +415,220 @@ public class VerdictController : ControllerBase
}
}
private (string? TenantId, ActionResult? Error) ResolveTenantContext(ClaimsPrincipal principal, IHeaderDictionary headers)
{
var tenantId = principal.FindFirst("tenant_id")?.Value
?? principal.FindFirst("tenant")?.Value;
if (string.IsNullOrWhiteSpace(tenantId))
{
return (null, StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Tenant claim is required.",
detail: "Authenticated principal does not contain tenant_id or tenant claim.",
status: StatusCodes.Status403Forbidden,
code: "tenant_claim_missing")));
}
if (headers.TryGetValue("X-Tenant-Id", out var headerTenant) &&
headerTenant.Count > 0 &&
!string.Equals(headerTenant[0], tenantId, StringComparison.Ordinal))
{
return (null, StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Tenant mismatch detected.",
detail: "Tenant header does not match authenticated tenant claim.",
status: StatusCodes.Status403Forbidden,
code: "tenant_mismatch")));
}
return (tenantId, null);
}
private (VerdictAuthorityKeyOptions? Entry, ActionResult? Error) ResolveRosterEntry(string keyId)
{
if (_verdictRosterOptions.Keys.Count == 0)
{
return (null, StatusCode(
StatusCodes.Status503ServiceUnavailable,
CreateProblem(
title: "Authority roster is unavailable.",
detail: "attestor:verdictTrust:keys must include at least one trusted key.",
status: StatusCodes.Status503ServiceUnavailable,
code: "authority_roster_unavailable")));
}
var entry = _verdictRosterOptions.Keys
.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal));
if (entry is null)
{
return (null, StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Signing key is not in authority roster.",
detail: $"Key '{keyId}' is not trusted for verdict creation.",
status: StatusCodes.Status403Forbidden,
code: "authority_key_unknown")));
}
if (string.Equals(entry.Status, "revoked", StringComparison.OrdinalIgnoreCase))
{
return (null, StatusCode(
StatusCodes.Status403Forbidden,
CreateProblem(
title: "Signing key is revoked.",
detail: $"Key '{entry.KeyId}' is revoked in authority roster.",
status: StatusCodes.Status403Forbidden,
code: "authority_key_revoked")));
}
if (string.IsNullOrWhiteSpace(entry.PublicKeyPem))
{
return (null, StatusCode(
StatusCodes.Status500InternalServerError,
CreateProblem(
title: "Authority roster key is incomplete.",
detail: $"Key '{entry.KeyId}' is missing public key material.",
status: StatusCodes.Status500InternalServerError,
code: "authority_key_missing_public_key")));
}
return (entry, null);
}
private async Task<VerdictLookupResponseDto?> FetchVerdictFromEvidenceLockerAsync(
string verdictId,
string tenantId,
CancellationToken ct)
{
if (_httpClientFactory is null)
{
return null;
}
try
{
var client = _httpClientFactory.CreateClient("EvidenceLocker");
var response = await client.GetAsync($"/api/v1/verdicts/{Uri.EscapeDataString(verdictId)}", ct).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Evidence Locker verdict lookup failed for {VerdictId}: {StatusCode}",
verdictId,
response.StatusCode);
return null;
}
var payload = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct).ConfigureAwait(false);
if (payload.ValueKind != JsonValueKind.Object)
{
return null;
}
var lockerTenant = GetOptionalString(payload, "tenant_id", "tenantId");
if (!string.IsNullOrWhiteSpace(lockerTenant) &&
!string.Equals(lockerTenant, tenantId, StringComparison.Ordinal))
{
return null;
}
var envelope = ExtractEnvelope(payload);
if (string.IsNullOrWhiteSpace(envelope))
{
return null;
}
var keyId = GetOptionalString(payload, "key_id", "keyId") ?? "unknown";
var createdAt = GetOptionalString(payload, "evaluated_at", "created_at", "createdAt")
?? _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
return new VerdictLookupResponseDto
{
VerdictId = verdictId,
AttestationUri = $"/api/v1/verdicts/{verdictId}",
Envelope = envelope,
KeyId = keyId,
CreatedAt = createdAt,
TenantId = tenantId
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Evidence Locker verdict lookup failed for {VerdictId}", verdictId);
return null;
}
}
private static string? ExtractEnvelope(JsonElement payload)
{
if (!payload.TryGetProperty("envelope", out var envelopeElement))
{
return null;
}
if (envelopeElement.ValueKind == JsonValueKind.String)
{
return envelopeElement.GetString();
}
if (envelopeElement.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
{
var envelopeJson = envelopeElement.GetRawText();
return Convert.ToBase64String(Encoding.UTF8.GetBytes(envelopeJson));
}
return null;
}
private static string? GetOptionalString(JsonElement payload, params string[] candidates)
{
foreach (var candidate in candidates)
{
if (payload.TryGetProperty(candidate, out var value) &&
value.ValueKind == JsonValueKind.String)
{
var text = value.GetString();
if (!string.IsNullOrWhiteSpace(text))
{
return text;
}
}
}
return null;
}
private static ProblemDetails CreateProblem(
string title,
string detail,
int status,
string code,
string[]? issues = null)
{
var problem = new ProblemDetails
{
Title = title,
Detail = detail,
Status = status
};
problem.Extensions["code"] = code;
if (issues is not null && issues.Length > 0)
{
problem.Extensions["issues"] = issues;
}
return problem;
}
/// <summary>
/// Extracts verdict metadata from predicate JSON.
/// </summary>
@@ -418,4 +737,28 @@ public class VerdictController : ControllerBase
StatusCode = StatusCodes.Status501NotImplemented
};
}
private sealed record CachedVerdictRecord(
string TenantId,
string Envelope,
string KeyId,
string CreatedAt)
{
public static CachedVerdictRecord From(VerdictAttestationResponseDto response, string tenantId)
=> new(tenantId, response.Envelope, response.KeyId, response.CreatedAt);
public static CachedVerdictRecord From(VerdictLookupResponseDto response)
=> new(response.TenantId, response.Envelope, response.KeyId, response.CreatedAt);
public VerdictLookupResponseDto ToLookupResponse(string verdictId)
=> new()
{
VerdictId = verdictId,
AttestationUri = $"/api/v1/verdicts/{verdictId}",
Envelope = Envelope,
KeyId = KeyId,
CreatedAt = CreatedAt,
TenantId = TenantId
};
}
}

View File

@@ -0,0 +1,15 @@
namespace StellaOps.Attestor.WebService.Options;
public sealed class VerdictAuthorityRosterOptions
{
public List<VerdictAuthorityKeyOptions> Keys { get; set; } = [];
}
public sealed class VerdictAuthorityKeyOptions
{
public string KeyId { get; set; } = string.Empty;
public string Status { get; set; } = "trusted";
public string PublicKeyPem { get; set; } = string.Empty;
}

View File

@@ -18,6 +18,7 @@
<PackageReference Include="StackExchange.Redis" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../StellaOps.Attestation/StellaOps.Attestation.csproj" />
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
<ProjectReference Include="..\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />

View File

@@ -5,6 +5,9 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| Task ID | Status | Notes |
| --- | --- | --- |
| ATTESTOR-225-002 | DOING | Sprint 225: enforce roster-based trust verification before verdict append. |
| ATTESTOR-225-003 | DOING | Sprint 225: resolve tenant from authenticated claims and block spoofing. |
| ATTESTOR-225-004 | DOING | Sprint 225: implement verdict-by-hash retrieval and tenant-scoped access checks. |
| AUDIT-0072-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
| AUDIT-0072-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
| AUDIT-0072-A | DONE | Applied 2026-01-13 (feature gating, correlation ID provider, proof chain/verification summary updates, tests). |

View File

@@ -7,6 +7,6 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Provenance\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj" />
<ProjectReference Include="..\..\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More