Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling.
- Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options.
- Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation.
- Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios.
- Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling.
- Included tests for UdpTransportOptions to verify default values and modification capabilities.
- Enhanced service registration tests for Udp transport services in the dependency injection container.
This commit is contained in:
master
2025-12-05 19:01:12 +02:00
parent 53508ceccb
commit cc69d332e3
245 changed files with 22440 additions and 27719 deletions

106
docs/contracts/README.md Normal file
View File

@@ -0,0 +1,106 @@
# StellaOps Contracts
This directory contains formal contract specifications for cross-module interfaces. These contracts define the data models, APIs, and integration points used throughout StellaOps.
## Purpose
Contracts serve as the authoritative source for:
- Data model definitions (request/response shapes)
- API endpoint specifications
- Integration requirements between modules
- Dependency documentation for sprint planning
## Contract Index
| Contract | ID | Unblocks | Status |
|----------|-----|----------|--------|
| [Advisory Key](./advisory-key.md) | CONTRACT-ADVISORY-KEY-001 | 6+ tasks | Published |
| [Risk Scoring](./risk-scoring.md) | CONTRACT-RISK-SCORING-002 | 5+ tasks | Published |
| [Mirror Bundle](./mirror-bundle.md) | CONTRACT-MIRROR-BUNDLE-003 | 8+ tasks | Published |
| [Sealed Mode](./sealed-mode.md) | CONTRACT-SEALED-MODE-004 | 4+ tasks | Published |
| [VEX Lens](./vex-lens.md) | CONTRACT-VEX-LENS-005 | 2+ tasks | Published |
| [Verification Policy](./verification-policy.md) | CONTRACT-VERIFICATION-POLICY-006 | 4+ tasks | Published |
| [Policy Studio](./policy-studio.md) | CONTRACT-POLICY-STUDIO-007 | 3+ tasks | Published |
| [Authority Effective Write](./authority-effective-write.md) | CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008 | 2+ tasks | Published |
| [Export Bundle](./export-bundle.md) | CONTRACT-EXPORT-BUNDLE-009 | 1+ tasks | Published |
| [Crypto Provider Registry](./crypto-provider-registry.md) | CONTRACT-CRYPTO-PROVIDER-REGISTRY-010 | 1+ tasks | Published |
| [Findings Ledger RLS](./findings-ledger-rls.md) | CONTRACT-FINDINGS-LEDGER-RLS-011 | 2 tasks | Published |
| [API Governance Baseline](./api-governance-baseline.md) | CONTRACT-API-GOVERNANCE-BASELINE-012 | 10+ tasks | Published |
| [Scanner PHP Analyzer](./scanner-php-analyzer.md) | CONTRACT-SCANNER-PHP-ANALYZER-013 | 1 task | Published |
| [Scanner Surface](./scanner-surface.md) | CONTRACT-SCANNER-SURFACE-014 | 1 task | Published |
| [RichGraph v1](./richgraph-v1.md) | CONTRACT-RICHGRAPH-V1-015 | 40+ tasks | Published |
## Contract Categories
### Core Data Models
- [Advisory Key](./advisory-key.md) - Vulnerability ID canonicalization
- [VEX Lens](./vex-lens.md) - VEX observation correlation
- [Risk Scoring](./risk-scoring.md) - Finding prioritization
### Air-Gap / Offline
- [Mirror Bundle](./mirror-bundle.md) - Bundle format for offline transport
- [Sealed Mode](./sealed-mode.md) - Sealed environment operation
### Security / Attestation
- [Verification Policy](./verification-policy.md) - Attestation verification rules
- [Crypto Provider Registry](./crypto-provider-registry.md) - Pluggable crypto
### Policy Management
- [Policy Studio](./policy-studio.md) - Policy editing and compilation
- [Authority Effective Write](./authority-effective-write.md) - Policy attachment
### Export
- [Export Bundle](./export-bundle.md) - Scheduled export jobs
### Tenancy / Database
- [Findings Ledger RLS](./findings-ledger-rls.md) - Row-Level Security and partitioning
### SDK & API Governance
- [API Governance Baseline](./api-governance-baseline.md) - OpenAPI freeze and SDK generation
### Scanner
- [Scanner PHP Analyzer](./scanner-php-analyzer.md) - PHP language analyzer bootstrap
- [Scanner Surface](./scanner-surface.md) - Surface analysis framework
### Reachability / Evidence
- [RichGraph v1](./richgraph-v1.md) - Function-level reachability graph schema
## Related Resources
### API Documentation
- [Policy API](../api/policy.md)
- [Graph API](../api/graph.md)
### Module Architecture
- [Excititor Architecture](../modules/excititor/architecture.md)
- [Policy Engine Architecture](../modules/policy/architecture.md)
- [Attestor Architecture](../modules/attestor/architecture.md)
- [AirGap Documentation](../airgap/README.md)
### JSON Schemas
- [Mirror Bundle Schema](../schemas/mirror-bundle.schema.json)
- [Verification Policy Schema](../../src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json)
- [Risk Profile Schema](../../src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json)
## Contract Lifecycle
1. **Draft** - Contract under development
2. **Published** - Contract is stable and ready for implementation
3. **Deprecated** - Contract is being phased out
4. **Retired** - Contract is no longer valid
## Contributing
When updating contracts:
1. Increment version number
2. Update `Last Updated` date
3. Document breaking changes
4. Update `Unblocks` section if tasks change
5. Add cross-references to related contracts
## Sprint Integration
Contracts unblock BLOCKED tasks in sprint files. When a contract is published:
1. Update the sprint file task status from `BLOCKED` to `TODO`
2. Add note: `Unblocked by CONTRACT-xxx (docs/contracts/xxx.md)`
3. Remove the blocked reason

View File

@@ -0,0 +1,186 @@
# Advisory Key Canonicalization Contract
**Contract ID:** `CONTRACT-ADVISORY-KEY-001`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the canonicalization rules for advisory and vulnerability identifiers used throughout StellaOps. It ensures consistent correlation of VEX observations, policy findings, and risk assessments across different identifier formats.
## Implementation Reference
**Source:** `src/Excititor/__Libraries/StellaOps.Excititor.Core/Canonicalization/VexAdvisoryKeyCanonicalizer.cs`
## Data Model
### VexCanonicalAdvisoryKey
The canonical advisory key structure returned by the canonicalizer.
```csharp
public sealed record VexCanonicalAdvisoryKey
{
/// <summary>
/// The canonical advisory key used for correlation and storage.
/// </summary>
public string AdvisoryKey { get; }
/// <summary>
/// The scope/authority level of the advisory.
/// </summary>
public VexAdvisoryScope Scope { get; }
/// <summary>
/// Original and alias identifiers preserved for traceability.
/// </summary>
public ImmutableArray<VexAdvisoryLink> Links { get; }
}
```
### VexAdvisoryLink
Represents a link to an original or alias advisory identifier.
```csharp
public sealed record VexAdvisoryLink
{
/// <summary>
/// The advisory identifier value.
/// </summary>
public string Identifier { get; }
/// <summary>
/// The type of identifier (cve, ghsa, rhsa, dsa, usn, msrc, other).
/// </summary>
public string Type { get; }
/// <summary>
/// True if this is the original identifier provided at ingest time.
/// </summary>
public bool IsOriginal { get; }
}
```
### VexAdvisoryScope
The scope/authority level of an advisory.
| Value | Code | Description | Examples |
|-------|------|-------------|----------|
| `Global` | 1 | Global identifiers | CVE-2024-1234 |
| `Ecosystem` | 2 | Ecosystem-specific | GHSA-xxxx-xxxx-xxxx |
| `Vendor` | 3 | Vendor-specific | RHSA-2024:1234, ADV-2024-1234 |
| `Distribution` | 4 | Distribution-specific | DSA-1234-1, USN-1234-1 |
| `Unknown` | 0 | Unclassified | Custom identifiers |
## Canonicalization Rules
### Identifier Patterns
| Pattern | Regex | Scope | Type |
|---------|-------|-------|------|
| CVE | `^CVE-\d{4}-\d{4,}$` | Global | `cve` |
| GHSA | `^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$` | Ecosystem | `ghsa` |
| RHSA | `^RH[A-Z]{2}-\d{4}:\d+$` | Vendor | `rhsa` |
| DSA | `^DSA-\d+(-\d+)?$` | Distribution | `dsa` |
| USN | `^USN-\d+(-\d+)?$` | Distribution | `usn` |
| MSRC | `^(ADV\|CVE)-\d{4}-\d+$` | Vendor | `msrc` |
| Other | * | Unknown | `other` |
### Canonical Key Format
1. **CVE identifiers** remain unchanged as they are globally authoritative:
```
CVE-2024-1234 → CVE-2024-1234
```
2. **Non-CVE identifiers** are prefixed with a scope indicator:
```
GHSA-xxxx-xxxx-xxxx → ECO:GHSA-XXXX-XXXX-XXXX
RHSA-2024:1234 → VND:RHSA-2024:1234
DSA-1234-1 → DST:DSA-1234-1
custom-id → UNK:CUSTOM-ID
```
### Scope Prefixes
| Scope | Prefix |
|-------|--------|
| Ecosystem | `ECO:` |
| Vendor | `VND:` |
| Distribution | `DST:` |
| Unknown | `UNK:` |
## Usage
### Canonicalizing an Identifier
```csharp
var canonicalizer = new VexAdvisoryKeyCanonicalizer();
// Simple canonicalization
var result = canonicalizer.Canonicalize("CVE-2024-1234");
// result.AdvisoryKey = "CVE-2024-1234"
// result.Scope = VexAdvisoryScope.Global
// With aliases
var result = canonicalizer.Canonicalize(
"GHSA-xxxx-xxxx-xxxx",
aliases: new[] { "CVE-2024-1234" });
// result.AdvisoryKey = "ECO:GHSA-XXXX-XXXX-XXXX"
// result.Links contains both identifiers
```
### Extracting CVE from Aliases
```csharp
var cve = canonicalizer.ExtractCveFromAliases(
new[] { "GHSA-xxxx-xxxx-xxxx", "CVE-2024-1234" });
// cve = "CVE-2024-1234"
```
## JSON Serialization
```json
{
"advisory_key": "CVE-2024-1234",
"scope": "global",
"links": [
{
"identifier": "CVE-2024-1234",
"type": "cve",
"is_original": true
},
{
"identifier": "GHSA-xxxx-xxxx-xxxx",
"type": "ghsa",
"is_original": false
}
]
}
```
## Determinism Guarantees
1. **Case normalization:** All identifiers are normalized to uppercase internally
2. **Stable ordering:** Links are ordered by original first, then alphabetically
3. **Deduplication:** Duplicate aliases are removed during canonicalization
4. **Idempotence:** Canonicalizing the same input always produces the same output
## Unblocks
This contract unblocks the following tasks:
- EXCITITOR-POLICY-20-001
- EXCITITOR-POLICY-20-002
- EXCITITOR-VULN-29-001
- EXCITITOR-VULN-29-002
- EXCITITOR-VULN-29-004
- CONCELIER-VEXLENS-30-001
## Related Contracts
- [VEX Lens Contract](./vex-lens.md) - Uses advisory keys for linkset correlation
- [Risk Scoring Contract](./risk-scoring.md) - References advisory IDs in findings

View File

@@ -0,0 +1,292 @@
# CONTRACT-API-GOVERNANCE-BASELINE-012: Aggregate OpenAPI Spec & SDK Generation
> **Status:** Published
> **Version:** 1.0.0
> **Published:** 2025-12-05
> **Owners:** API Governance Guild, SDK Generator Guild
> **Unblocks:** SDKGEN-63-001, SDKGEN-63-002, SDKGEN-63-003, SDKGEN-63-004, SDKGEN-64-001, SDKGEN-64-002
## Overview
This contract defines the aggregate OpenAPI specification freeze process, versioning rules, and SHA256 commitment mechanism that enables deterministic SDK generation across TypeScript, Python, Go, and Java targets.
## Aggregate Specification
### Source Location
```
src/Api/StellaOps.Api.OpenApi/stella.yaml
```
### Composition Process
The aggregate spec is generated by `compose.mjs` from per-service specs:
| Service | Source Spec | Tag Prefix |
|---------|-------------|------------|
| Authority | `authority/openapi.yaml` | `authority.*` |
| Export Center | `export-center/openapi.yaml` | `export.*` |
| Graph | `graph/openapi.yaml` | `graph.*` |
| Orchestrator | `orchestrator/openapi.yaml` | `orchestrator.*` |
| Policy | `policy/openapi.yaml` | `policy.*` |
| Scheduler | `scheduler/openapi.yaml` | `scheduler.*` |
### Current Version
```yaml
openapi: 3.1.0
info:
title: StellaOps Aggregate API
version: 0.0.1
```
---
## Freeze Process
### 1. Version Tagging
When freezing for SDK generation:
```bash
# Compute SHA256 of aggregate spec
sha256sum src/Api/StellaOps.Api.OpenApi/stella.yaml > stella.yaml.sha256
# Tag the commit
git tag -a api/v0.1.0-alpha -m "API freeze for SDK Wave B generation"
```
### 2. SHA256 Commitment
SDK generators must validate the spec hash before generation:
```bash
# Environment variable for hash guard
export STELLA_OAS_EXPECTED_SHA256="<sha256-hash>"
# Generator validates before running
if [ "$(sha256sum stella.yaml | cut -d' ' -f1)" != "$STELLA_OAS_EXPECTED_SHA256" ]; then
echo "ERROR: Spec hash mismatch - regenerate after spec freeze"
exit 1
fi
```
### 3. Published Artifacts
On freeze, publish:
| Artifact | Location | Purpose |
|----------|----------|---------|
| Tagged spec | `api/v{version}` git tag | Version reference |
| SHA256 file | `stella.yaml.sha256` | Hash verification |
| Changelog | `CHANGELOG-api.md` | Breaking changes |
---
## SDK Generation Contract
### Generator Configuration
| Language | Config | Output |
|----------|--------|--------|
| TypeScript | `ts/config.yaml` | ESM/CJS with typed errors |
| Python | `python/config.yaml` | sync/async clients, type hints |
| Go | `go/config.yaml` | context-first API |
| Java | `java/config.yaml` | builder pattern, OkHttp |
### Toolchain Lock
```yaml
# toolchain.lock.yaml
openapi-generator-cli: 7.4.0
jdk: 21.0.1
node: 22.x
python: 3.11+
go: 1.21+
```
### Hash Guard Implementation
Each generator emits `.oas.sha256` for provenance:
```bash
# Example: TypeScript generation
echo "$STELLA_OAS_EXPECTED_SHA256 stella.yaml" > dist/.oas.sha256
```
---
## Versioning Rules
### Semantic Versioning
```
MAJOR.MINOR.PATCH[-PRERELEASE]
- MAJOR: Breaking API changes
- MINOR: New endpoints/fields (backwards compatible)
- PATCH: Bug fixes, documentation
- PRERELEASE: alpha, beta, rc
```
### Breaking Change Detection
```bash
# Run API compatibility check
npm run api:compat -- --old scripts/__fixtures__/api-compat/old.yaml \
--new src/Api/StellaOps.Api.OpenApi/stella.yaml
```
### Version Matrix
| API Version | SDK Versions | Status |
|-------------|--------------|--------|
| 0.0.1 | - | Current (unfrozen) |
| 0.1.0-alpha | TS/Py/Go/Java alpha | Target freeze |
---
## Freeze Checklist
Before SDK generation can proceed:
- [ ] All per-service specs pass `npm run api:lint`
- [ ] Aggregate composition succeeds (`node compose.mjs`)
- [ ] Breaking change review completed
- [ ] SHA256 computed and committed
- [ ] Git tag created (`api/v{version}`)
- [ ] Changelog entry added
- [ ] SDK generator configs updated with hash
---
## Current Freeze Status
### Pending Actions
| Action | Owner | Due | Status |
|--------|-------|-----|--------|
| Compute SHA256 for stella.yaml | API Governance Guild | 2025-12-06 | TODO |
| Create api/v0.1.0-alpha tag | API Governance Guild | 2025-12-06 | TODO |
| Update SDKGEN configs with hash | SDK Generator Guild | 2025-12-06 | TODO |
### Immediate Unblock Path
To immediately unblock SDK generation:
```bash
# 1. Compute current spec hash
cd src/Api/StellaOps.Api.OpenApi
SHA=$(sha256sum stella.yaml | cut -d' ' -f1)
echo "Current SHA256: $SHA"
# 2. Create hash file
echo "$SHA stella.yaml" > stella.yaml.sha256
# 3. Tag for SDK generation
git add stella.yaml.sha256
git commit -m "chore(api): freeze aggregate spec for SDK Wave B"
git tag -a api/v0.1.0-alpha -m "API freeze for SDK generation"
# 4. Set environment for generators
export STELLA_OAS_EXPECTED_SHA256="$SHA"
```
---
## SDK Generation Commands
Once freeze is complete:
```bash
# TypeScript
cd src/Sdk/StellaOps.Sdk.Generator/ts
./generate-ts.sh
# Python
cd ../python
./generate-python.sh
# Go
cd ../go
./generate-go.sh
# Java
cd ../java
./generate-java.sh
# Run all smoke tests
npm run sdk:smoke
```
---
## Governance
### Change Process
1. **Propose:** Open PR with spec changes
2. **Review:** API Governance Guild reviews for breaking changes
3. **Test:** Run `api:lint` and `api:compat`
4. **Merge:** Merge to main
5. **Freeze:** Tag and compute SHA256 when ready for SDK
### Stakeholders
- **API Governance Guild:** Spec ownership, breaking change review
- **SDK Generator Guild:** Generation toolchain, language packs
- **Platform Security:** Signing key provisioning (SDKREL-63-001)
---
## Signing Keys
### Development Key (Available Now)
A development signing key is available for staging/testing:
| File | Purpose |
|------|---------|
| `tools/cosign/cosign.dev.key` | Private key (password: `stellaops-dev`) |
| `tools/cosign/cosign.dev.pub` | Public key for verification |
**Usage for SDK staging:**
```bash
# Set environment for SDK signing
export COSIGN_KEY_FILE=tools/cosign/cosign.dev.key
export COSIGN_PASSWORD=stellaops-dev
export COSIGN_ALLOW_DEV_KEY=1
# Or use CI workflow with allow_dev_key=1
```
### Production Keys (Pending)
Production signing requires:
- Sovereign crypto key provisioning (Action #7)
- `COSIGN_PRIVATE_KEY_B64` CI secret
- Optional `COSIGN_PASSWORD` for encrypted keys
### Key Resolution Order
1. `COSIGN_KEY_FILE` environment variable
2. `COSIGN_PRIVATE_KEY_B64` (decoded to temp file)
3. `tools/cosign/cosign.key` (production drop-in)
4. `tools/cosign/cosign.dev.key` (only if `COSIGN_ALLOW_DEV_KEY=1`)
---
## Reference
- Aggregate spec: `src/Api/StellaOps.Api.OpenApi/stella.yaml`
- Composition script: `src/Api/StellaOps.Api.OpenApi/compose.mjs`
- Toolchain lock: `src/Sdk/StellaOps.Sdk.Generator/TOOLCHAIN.md`
- SDK generators: `src/Sdk/StellaOps.Sdk.Generator/{ts,python,go,java}/`
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-05 | API Governance Guild | Initial contract |

View File

@@ -0,0 +1,271 @@
# Authority Effective Write Contract
**Contract ID:** `CONTRACT-AUTHORITY-EFFECTIVE-WRITE-008`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the `effective:write` scope and associated APIs for managing effective policies and scope attachments in the Authority module. It enables attaching policies to subjects with priority and expiration rules.
## Implementation References
- **Authority Module:** `src/Authority/`
- **API Spec:** `src/Api/StellaOps.Api.OpenApi/authority/openapi.yaml`
## Scope Definition
### effective:write
Grants permission to:
- Create and update effective policies
- Attach scopes to policies
- Manage policy priorities and expiration
## Data Models
### EffectivePolicy
```json
{
"effective_policy_id": "eff-001",
"tenant_id": "default",
"policy_id": "policy-001",
"policy_version": "1.0.0",
"subject_pattern": "pkg:npm/*",
"priority": 100,
"enabled": true,
"expires_at": "2025-12-31T23:59:59Z",
"scopes": ["scan:read", "scan:write"],
"created_at": "2025-12-05T10:00:00Z",
"created_by": "admin@example.com",
"updated_at": "2025-12-05T10:00:00Z"
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `effective_policy_id` | string | Auto | Unique identifier |
| `tenant_id` | string | Yes | Tenant scope |
| `policy_id` | string | Yes | Referenced policy |
| `policy_version` | string | No | Specific version (latest if omitted) |
| `subject_pattern` | string | Yes | Subject matching pattern |
| `priority` | integer | Yes | Priority (higher = more important) |
| `enabled` | boolean | No | Whether policy is active (default: true) |
| `expires_at` | datetime | No | Optional expiration time |
| `scopes` | array | No | Attached authorization scopes |
### ScopeAttachment
```json
{
"attachment_id": "att-001",
"effective_policy_id": "eff-001",
"scope": "scan:write",
"conditions": {
"repository_pattern": "github.com/org/*"
},
"created_at": "2025-12-05T10:00:00Z"
}
```
### Subject Patterns
Subject patterns use glob-style matching:
| Pattern | Matches |
|---------|---------|
| `*` | All subjects |
| `pkg:npm/*` | All npm packages |
| `pkg:npm/@org/*` | Npm packages in @org scope |
| `pkg:maven/com.example/*` | Maven packages in com.example |
| `oci://registry.example.com/*` | All images in registry |
## API Endpoints
### Effective Policies
#### Create Effective Policy
```
POST /api/v1/authority/effective-policies
Content-Type: application/json
Authorization: Bearer <token with effective:write scope>
{
"tenant_id": "default",
"policy_id": "security-policy-v1",
"subject_pattern": "pkg:npm/*",
"priority": 100,
"scopes": ["scan:read", "scan:write"]
}
Response: 201 Created
{
"effective_policy_id": "eff-001",
"tenant_id": "default",
"policy_id": "security-policy-v1",
"subject_pattern": "pkg:npm/*",
"priority": 100,
"enabled": true,
"scopes": ["scan:read", "scan:write"],
"created_at": "2025-12-05T10:00:00Z"
}
```
#### Update Effective Policy
```
PUT /api/v1/authority/effective-policies/{effective_policy_id}
Content-Type: application/json
Authorization: Bearer <token with effective:write scope>
{
"priority": 150,
"expires_at": "2025-12-31T23:59:59Z"
}
Response: 200 OK
```
#### Delete Effective Policy
```
DELETE /api/v1/authority/effective-policies/{effective_policy_id}
Authorization: Bearer <token with effective:write scope>
Response: 204 No Content
```
#### List Effective Policies
```
GET /api/v1/authority/effective-policies?tenant_id=default
Response: 200 OK
{
"items": [
{
"effective_policy_id": "eff-001",
"policy_id": "security-policy-v1",
"subject_pattern": "pkg:npm/*",
"priority": 100
}
],
"total": 1
}
```
### Scope Attachments
#### Attach Scope
```
POST /api/v1/authority/scope-attachments
Content-Type: application/json
Authorization: Bearer <token with effective:write scope>
{
"effective_policy_id": "eff-001",
"scope": "promotion:approve",
"conditions": {
"environment": "production"
}
}
Response: 201 Created
{
"attachment_id": "att-001",
"effective_policy_id": "eff-001",
"scope": "promotion:approve",
"conditions": {...}
}
```
#### Detach Scope
```
DELETE /api/v1/authority/scope-attachments/{attachment_id}
Authorization: Bearer <token with effective:write scope>
Response: 204 No Content
```
### Policy Resolution
#### Resolve Effective Policy for Subject
```
GET /api/v1/authority/resolve?subject=pkg:npm/lodash@4.17.20
Response: 200 OK
{
"subject": "pkg:npm/lodash@4.17.20",
"effective_policy": {
"effective_policy_id": "eff-001",
"policy_id": "security-policy-v1",
"policy_version": "1.0.0",
"priority": 100
},
"granted_scopes": ["scan:read", "scan:write"],
"matched_pattern": "pkg:npm/*"
}
```
## Priority Resolution
When multiple effective policies match a subject:
1. Higher `priority` value wins
2. If equal priority, more specific pattern wins
3. If equal specificity, most recently updated wins
Example:
```
Pattern: pkg:npm/* Priority: 100 → Matches
Pattern: pkg:npm/@org/* Priority: 50 → Matches (more specific)
Pattern: pkg:* Priority: 200 → Matches
Winner: pkg:npm/@org/* (most specific among matches)
```
## Audit Trail
All effective:write operations are logged:
```json
{
"event": "effective_policy.created",
"effective_policy_id": "eff-001",
"actor": "admin@example.com",
"timestamp": "2025-12-05T10:00:00Z",
"changes": {
"policy_id": "security-policy-v1",
"subject_pattern": "pkg:npm/*"
}
}
```
## Error Codes
| Code | Message |
|------|---------|
| `ERR_AUTH_001` | Invalid subject pattern |
| `ERR_AUTH_002` | Policy not found |
| `ERR_AUTH_003` | Duplicate attachment |
| `ERR_AUTH_004` | Invalid scope |
| `ERR_AUTH_005` | Priority conflict |
## Unblocks
This contract unblocks the following tasks:
- POLICY-AOC-19-002
- POLICY-AOC-19-003
- POLICY-AOC-19-004
## Related Contracts
- [Policy Studio Contract](./policy-studio.md) - Policy creation
- [Verification Policy Contract](./verification-policy.md) - Attestation policies

View File

@@ -0,0 +1,294 @@
# Crypto Provider Registry Contract
**Contract ID:** `CONTRACT-CRYPTO-PROVIDER-REGISTRY-010`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the ICryptoProviderRegistry interface for managing cryptographic providers across StellaOps modules. It supports pluggable crypto implementations including .NET default, FIPS 140-2, GOST (CryptoPro), and Chinese SM algorithms.
## Implementation References
- **Registry:** `src/Security/StellaOps.Security.Crypto/`
- **Providers:** `src/Security/StellaOps.Security.Crypto.Providers/`
## Interface Definition
### ICryptoProviderRegistry
```csharp
public interface ICryptoProviderRegistry
{
/// <summary>
/// Registers a crypto provider with the given identifier.
/// </summary>
void RegisterProvider(string providerId, ICryptoProvider provider);
/// <summary>
/// Gets a registered crypto provider by identifier.
/// </summary>
ICryptoProvider GetProvider(string providerId);
/// <summary>
/// Gets the default crypto provider.
/// </summary>
ICryptoProvider GetDefaultProvider();
/// <summary>
/// Lists all registered provider information.
/// </summary>
IReadOnlyList<CryptoProviderInfo> ListProviders();
/// <summary>
/// Checks if a provider is registered.
/// </summary>
bool HasProvider(string providerId);
}
```
### ICryptoProvider
```csharp
public interface ICryptoProvider
{
/// <summary>
/// Provider identifier.
/// </summary>
string ProviderId { get; }
/// <summary>
/// Provider display name.
/// </summary>
string DisplayName { get; }
/// <summary>
/// Supported algorithms.
/// </summary>
IReadOnlyList<string> SupportedAlgorithms { get; }
/// <summary>
/// Creates a hash algorithm instance.
/// </summary>
HashAlgorithm CreateHashAlgorithm(string algorithm);
/// <summary>
/// Creates a signature algorithm instance.
/// </summary>
AsymmetricAlgorithm CreateSignatureAlgorithm(string algorithm);
/// <summary>
/// Creates a key derivation function instance.
/// </summary>
KeyDerivationPrf CreateKdf(string algorithm);
}
```
### CryptoProviderInfo
```json
{
"provider_id": "fips",
"display_name": "FIPS 140-2 Provider",
"version": "1.0.0",
"supported_algorithms": [
"SHA-256", "SHA-384", "SHA-512",
"RSA-PSS", "ECDSA-P256", "ECDSA-P384"
],
"compliance": ["FIPS 140-2"],
"is_default": false
}
```
## Available Providers
### Default Provider
Standard .NET cryptography implementation.
| Provider ID | `default` |
|-------------|-----------|
| **Display Name** | .NET Cryptography |
| **Algorithms** | SHA-256, SHA-384, SHA-512, RSA, ECDSA, EdDSA |
| **Compliance** | None (platform default) |
### FIPS Provider
FIPS 140-2 validated cryptographic module.
| Provider ID | `fips` |
|-------------|--------|
| **Display Name** | FIPS 140-2 Provider |
| **Algorithms** | SHA-256, SHA-384, SHA-512, RSA-PSS, ECDSA-P256, ECDSA-P384 |
| **Compliance** | FIPS 140-2 |
### GOST Provider (CryptoPro)
Russian GOST cryptographic algorithms via CryptoPro CSP.
| Provider ID | `gost` |
|-------------|--------|
| **Display Name** | CryptoPro GOST |
| **Algorithms** | GOST R 34.11-2012 (Stribog), GOST R 34.10-2012 |
| **Compliance** | GOST, eIDAS (Russia) |
### SM Provider (China)
Chinese cryptographic algorithms.
| Provider ID | `sm` |
|-------------|------|
| **Display Name** | SM Crypto (China) |
| **Algorithms** | SM2 (signature), SM3 (hash), SM4 (encryption) |
| **Compliance** | GB/T (China National Standard) |
## Configuration
### Registration at Startup
```csharp
services.AddCryptoProviderRegistry(options =>
{
options.DefaultProvider = "default";
options.RegisterProvider<FipsCryptoProvider>("fips");
options.RegisterProvider<GostCryptoProvider>("gost");
options.RegisterProvider<SmCryptoProvider>("sm");
});
```
### Provider Selection
```csharp
var registry = serviceProvider.GetRequiredService<ICryptoProviderRegistry>();
// Get specific provider
var fipsProvider = registry.GetProvider("fips");
// Get default provider
var defaultProvider = registry.GetDefaultProvider();
// List all providers
var providers = registry.ListProviders();
```
## API Endpoints
### List Providers
```
GET /api/v1/crypto/providers
Response: 200 OK
{
"providers": [
{
"provider_id": "default",
"display_name": ".NET Cryptography",
"supported_algorithms": [...],
"is_default": true
},
{
"provider_id": "fips",
"display_name": "FIPS 140-2 Provider",
"supported_algorithms": [...],
"compliance": ["FIPS 140-2"]
}
]
}
```
### Get Provider Details
```
GET /api/v1/crypto/providers/{provider_id}
Response: 200 OK
{
"provider_id": "fips",
"display_name": "FIPS 140-2 Provider",
"version": "1.0.0",
"supported_algorithms": [
"SHA-256", "SHA-384", "SHA-512",
"RSA-PSS", "ECDSA-P256", "ECDSA-P384"
],
"compliance": ["FIPS 140-2"]
}
```
### Set Default Provider
```
PUT /api/v1/crypto/providers/default
Content-Type: application/json
{
"provider_id": "fips"
}
Response: 200 OK
```
## Algorithm Mapping
### Hash Algorithms
| Algorithm | Default | FIPS | GOST | SM |
|-----------|---------|------|------|-----|
| SHA-256 | Yes | Yes | No | No |
| SHA-384 | Yes | Yes | No | No |
| SHA-512 | Yes | Yes | No | No |
| GOST R 34.11-2012 (256) | No | No | Yes | No |
| GOST R 34.11-2012 (512) | No | No | Yes | No |
| SM3 | No | No | No | Yes |
### Signature Algorithms
| Algorithm | Default | FIPS | GOST | SM |
|-----------|---------|------|------|-----|
| RSA-PSS | Yes | Yes | No | No |
| ECDSA-P256 | Yes | Yes | No | No |
| ECDSA-P384 | Yes | Yes | No | No |
| EdDSA | Yes | No | No | No |
| GOST R 34.10-2012 | No | No | Yes | No |
| SM2 | No | No | No | Yes |
## Usage Example
### Signing with GOST
```csharp
var registry = services.GetRequiredService<ICryptoProviderRegistry>();
var gostProvider = registry.GetProvider("gost");
using var algorithm = gostProvider.CreateSignatureAlgorithm("GOST R 34.10-2012");
var signature = algorithm.SignData(data, HashAlgorithmName.SHA256);
```
### Hashing with SM3
```csharp
var smProvider = registry.GetProvider("sm");
using var hash = smProvider.CreateHashAlgorithm("SM3");
var digest = hash.ComputeHash(data);
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `STELLAOPS_CRYPTO_DEFAULT_PROVIDER` | Default provider ID |
| `StellaOpsEnableCryptoPro` | Enable CryptoPro GOST (set to `true`) |
| `StellaOpsEnableSmCrypto` | Enable SM crypto (set to `true`) |
| `STELLAOPS_FIPS_MODE` | Enable FIPS mode |
## Unblocks
This contract unblocks the following tasks:
- EXCITITOR-CRYPTO-90-001
## Related Contracts
- [Verification Policy Contract](./verification-policy.md) - Algorithm selection
- [Sealed Mode Contract](./sealed-mode.md) - Offline crypto validation

View File

@@ -0,0 +1,324 @@
# Export Bundle Scheduler Contract
**Contract ID:** `CONTRACT-EXPORT-BUNDLE-009`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the export bundle job scheduling and manifest format used by the Export Center. It covers job definitions, scheduling, output formats, and attestation integration.
## Implementation References
- **Export Center:** `src/ExportCenter/`
- **API Spec:** `src/Api/StellaOps.Api.OpenApi/export-center/openapi.yaml`
## Data Models
### ExportBundleJob
Job definition for scheduled exports.
```json
{
"job_id": "job-001",
"tenant_id": "default",
"name": "daily-vex-export",
"description": "Daily VEX advisory export",
"query": {
"type": "vex",
"filters": {
"severity": ["critical", "high"],
"providers": ["github", "redhat"]
}
},
"format": "openvex",
"schedule": "0 0 * * *",
"destination": {
"type": "s3",
"config": {
"bucket": "exports",
"prefix": "vex/daily/"
}
},
"signing": {
"enabled": true,
"predicate_type": "stella.ops/vex@v1"
},
"enabled": true,
"created_at": "2025-12-05T10:00:00Z",
"last_run_at": "2025-12-05T00:00:00Z",
"next_run_at": "2025-12-06T00:00:00Z"
}
```
### Export Formats
| Format | Description | MIME Type |
|--------|-------------|-----------|
| `openvex` | OpenVEX JSON | application/json |
| `csaf` | CSAF VEX | application/json |
| `cyclonedx` | CycloneDX VEX | application/json |
| `spdx` | SPDX document | application/json |
| `ndjson` | Newline-delimited JSON | application/x-ndjson |
| `json` | Standard JSON array | application/json |
### Schedule Format
Cron expressions (5 fields):
```
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
```
Examples:
| Schedule | Description |
|----------|-------------|
| `0 0 * * *` | Daily at midnight |
| `0 */6 * * *` | Every 6 hours |
| `0 0 * * 0` | Weekly on Sunday |
| `0 0 1 * *` | Monthly on the 1st |
### Destination Types
#### S3 Destination
```json
{
"type": "s3",
"config": {
"bucket": "my-exports",
"prefix": "vex/",
"region": "us-east-1",
"endpoint": "https://s3.amazonaws.com"
}
}
```
#### File Destination
```json
{
"type": "file",
"config": {
"path": "/exports/vex/"
}
}
```
#### Webhook Destination
```json
{
"type": "webhook",
"config": {
"url": "https://example.com/webhook",
"headers": {
"Authorization": "Bearer ${SECRET}"
}
}
}
```
### ExportBundleManifest
Manifest for completed export.
```json
{
"bundle_id": "bundle-001",
"job_id": "job-001",
"tenant_id": "default",
"created_at": "2025-12-05T00:00:00Z",
"format": "openvex",
"artifact_digest": "sha256:abc123...",
"artifact_size_bytes": 1048576,
"query_signature": "sha256:def456...",
"item_count": 150,
"policy_digest": "sha256:...",
"consensus_digest": "sha256:...",
"score_digest": "sha256:...",
"attestation": {
"predicate_type": "stella.ops/vex@v1",
"rekor_uuid": "24296fb24b8ad77a...",
"rekor_index": 12345,
"signed_at": "2025-12-05T00:00:01Z"
}
}
```
## API Endpoints
### Job Management
#### Create Export Job
```
POST /api/v1/export/jobs
Content-Type: application/json
Authorization: Bearer <token>
{
"name": "daily-vex-export",
"query": {...},
"format": "openvex",
"schedule": "0 0 * * *",
"destination": {...}
}
Response: 201 Created
{
"job_id": "job-001",
...
}
```
#### Update Job
```
PUT /api/v1/export/jobs/{job_id}
Content-Type: application/json
{
"schedule": "0 */12 * * *",
"enabled": true
}
Response: 200 OK
```
#### Delete Job
```
DELETE /api/v1/export/jobs/{job_id}
Response: 204 No Content
```
#### List Jobs
```
GET /api/v1/export/jobs?tenant_id=default
Response: 200 OK
{
"items": [...],
"total": 5
}
```
### Manual Execution
#### Trigger Job
```
POST /api/v1/export/jobs/{job_id}/run
Response: 202 Accepted
{
"execution_id": "exec-001",
"status": "running"
}
```
#### Get Execution Status
```
GET /api/v1/export/jobs/{job_id}/executions/{execution_id}
Response: 200 OK
{
"execution_id": "exec-001",
"status": "completed",
"bundle_id": "bundle-001",
"started_at": "2025-12-05T00:00:00Z",
"completed_at": "2025-12-05T00:00:05Z"
}
```
### Bundle Retrieval
#### Get Bundle Manifest
```
GET /api/v1/export/bundles/{bundle_id}
Response: 200 OK
{
"bundle_id": "bundle-001",
"artifact_digest": "sha256:...",
...
}
```
#### Download Bundle
```
GET /api/v1/export/bundles/{bundle_id}/download
Response: 200 OK
Content-Type: application/json
Content-Disposition: attachment; filename="vex-export-2025-12-05.json"
[bundle content]
```
## Signing Configuration
### Enable Signing
```json
{
"signing": {
"enabled": true,
"predicate_type": "stella.ops/vex@v1",
"key_id": "signing-key-001",
"include_rekor": true
}
}
```
### Predicate Types
| Type | Description |
|------|-------------|
| `stella.ops/vex@v1` | VEX export attestation |
| `stella.ops/sbom@v1` | SBOM export attestation |
| `stella.ops/policy@v1` | Policy result export |
## Job Status
| Status | Description |
|--------|-------------|
| `idle` | Job is waiting for next scheduled run |
| `running` | Job is currently executing |
| `completed` | Last run completed successfully |
| `failed` | Last run failed |
| `disabled` | Job is disabled |
## Error Codes
| Code | Message |
|------|---------|
| `ERR_EXP_001` | Invalid schedule expression |
| `ERR_EXP_002` | Invalid destination config |
| `ERR_EXP_003` | Export failed |
| `ERR_EXP_004` | Signing failed |
| `ERR_EXP_005` | Job not found |
## Unblocks
This contract unblocks the following tasks:
- EXPORT-CONSOLE-23-001
## Related Contracts
- [Mirror Bundle Contract](./mirror-bundle.md) - Bundle format for air-gap
- [Risk Scoring Contract](./risk-scoring.md) - Score digest in exports

View File

@@ -0,0 +1,416 @@
# CONTRACT-FINDINGS-LEDGER-RLS-011: Row-Level Security & Partitioning
> **Status:** Published
> **Version:** 1.0.0
> **Published:** 2025-12-05
> **Owners:** Platform/DB Guild, Findings Ledger Guild
> **Unblocks:** LEDGER-TEN-48-001-DEV, DEVOPS-LEDGER-TEN-48-001-REL
## Overview
This contract specifies the Row-Level Security (RLS) and partitioning strategy for the Findings Ledger module. It is based on the proven Evidence Locker implementation pattern and adapted for Findings Ledger's schema.
## Current State (Already Implemented)
The Findings Ledger already has these foundational elements:
### 1. LIST Partitioning by Tenant
All tables are partitioned by `tenant_id`:
```sql
-- Example from ledger_events
CREATE TABLE ledger_events (
tenant_id TEXT NOT NULL,
...
) PARTITION BY LIST (tenant_id);
```
**Tables with partitioning:**
- `ledger_events`
- `ledger_merkle_roots`
- `findings_projection`
- `finding_history`
- `triage_actions`
- `ledger_attestations`
- `orchestrator_exports`
- `airgap_imports`
### 2. Session Variable Configuration
Connection setup in `LedgerDataSource.cs`:
```csharp
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT set_config('app.current_tenant', @tenant, false);";
cmd.Parameters.AddWithValue("tenant", tenantId);
await cmd.ExecuteNonQueryAsync(ct);
```
### 3. HTTP Header Tenant Extraction
From `Program.cs`:
- Header: `X-Stella-Tenant`
- Validation: Non-empty required
- Error: 400 Bad Request if missing
### 4. Application-Level Query Filtering
All repository queries include `WHERE tenant_id = @tenant` (defense in depth).
---
## Required Implementation (The Missing 10%)
### 1. Tenant Validation Function
Create a schema function following the Evidence Locker pattern:
```sql
-- Schema for application-level functions
CREATE SCHEMA IF NOT EXISTS findings_ledger_app;
-- Tenant validation function (TEXT version for Ledger compatibility)
CREATE OR REPLACE FUNCTION findings_ledger_app.require_current_tenant()
RETURNS TEXT
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
tenant_text TEXT;
BEGIN
tenant_text := current_setting('app.current_tenant', true);
IF tenant_text IS NULL OR length(trim(tenant_text)) = 0 THEN
RAISE EXCEPTION 'app.current_tenant is not set for the current session'
USING ERRCODE = 'P0001';
END IF;
RETURN tenant_text;
END;
$$;
COMMENT ON FUNCTION findings_ledger_app.require_current_tenant() IS
'Returns the current tenant ID from session variable, raises exception if not set';
```
### 2. RLS Policies for All Tables
Apply to each tenant-scoped table:
```sql
-- ============================================
-- ledger_events
-- ============================================
ALTER TABLE ledger_events ENABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_events FORCE ROW LEVEL SECURITY;
CREATE POLICY ledger_events_tenant_isolation
ON ledger_events
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- ledger_merkle_roots
-- ============================================
ALTER TABLE ledger_merkle_roots ENABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_merkle_roots FORCE ROW LEVEL SECURITY;
CREATE POLICY ledger_merkle_roots_tenant_isolation
ON ledger_merkle_roots
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- findings_projection
-- ============================================
ALTER TABLE findings_projection ENABLE ROW LEVEL SECURITY;
ALTER TABLE findings_projection FORCE ROW LEVEL SECURITY;
CREATE POLICY findings_projection_tenant_isolation
ON findings_projection
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- finding_history
-- ============================================
ALTER TABLE finding_history ENABLE ROW LEVEL SECURITY;
ALTER TABLE finding_history FORCE ROW LEVEL SECURITY;
CREATE POLICY finding_history_tenant_isolation
ON finding_history
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- triage_actions
-- ============================================
ALTER TABLE triage_actions ENABLE ROW LEVEL SECURITY;
ALTER TABLE triage_actions FORCE ROW LEVEL SECURITY;
CREATE POLICY triage_actions_tenant_isolation
ON triage_actions
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- ledger_attestations
-- ============================================
ALTER TABLE ledger_attestations ENABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_attestations FORCE ROW LEVEL SECURITY;
CREATE POLICY ledger_attestations_tenant_isolation
ON ledger_attestations
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- orchestrator_exports
-- ============================================
ALTER TABLE orchestrator_exports ENABLE ROW LEVEL SECURITY;
ALTER TABLE orchestrator_exports FORCE ROW LEVEL SECURITY;
CREATE POLICY orchestrator_exports_tenant_isolation
ON orchestrator_exports
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
-- ============================================
-- airgap_imports
-- ============================================
ALTER TABLE airgap_imports ENABLE ROW LEVEL SECURITY;
ALTER TABLE airgap_imports FORCE ROW LEVEL SECURITY;
CREATE POLICY airgap_imports_tenant_isolation
ON airgap_imports
FOR ALL
USING (tenant_id = findings_ledger_app.require_current_tenant())
WITH CHECK (tenant_id = findings_ledger_app.require_current_tenant());
```
### 3. System/Admin Bypass Role
For migrations and cross-tenant admin operations:
```sql
-- Create admin role that bypasses RLS
CREATE ROLE findings_ledger_admin NOLOGIN;
-- Grant bypass to admin role
ALTER ROLE findings_ledger_admin BYPASSRLS;
-- Application service account for migrations
GRANT findings_ledger_admin TO stellaops_migration_user;
```
---
## Connection Patterns
### Regular Connections (Tenant-Scoped)
```csharp
public async Task<NpgsqlConnection> OpenTenantConnectionAsync(
string tenantId,
CancellationToken ct)
{
var connection = await _dataSource.OpenConnectionAsync(ct);
await using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT set_config('app.current_tenant', @tenant, false)";
cmd.Parameters.AddWithValue("tenant", tenantId);
await cmd.ExecuteNonQueryAsync(ct);
return connection;
}
```
### System Connections (No Tenant - Migrations Only)
```csharp
public async Task<NpgsqlConnection> OpenSystemConnectionAsync(CancellationToken ct)
{
// Uses admin role, no tenant set
// ONLY for: migrations, health checks, cross-tenant admin ops
var connection = await _adminDataSource.OpenConnectionAsync(ct);
return connection;
}
```
---
## Compliance Validation
### Pre-Deployment Checks
```sql
-- 1. Verify RLS enabled on all tables
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN (
'ledger_events', 'ledger_merkle_roots', 'findings_projection',
'finding_history', 'triage_actions', 'ledger_attestations',
'orchestrator_exports', 'airgap_imports'
)
AND rowsecurity = false;
-- Expected: 0 rows (all should have RLS enabled)
-- 2. Verify policies exist for all tables
SELECT tablename, policyname
FROM pg_policies
WHERE schemaname = 'public'
AND tablename IN (
'ledger_events', 'ledger_merkle_roots', 'findings_projection',
'finding_history', 'triage_actions', 'ledger_attestations',
'orchestrator_exports', 'airgap_imports'
);
-- Expected: 8 rows (one policy per table)
-- 3. Verify tenant validation function exists
SELECT proname, prosrc
FROM pg_proc
WHERE proname = 'require_current_tenant'
AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'findings_ledger_app');
-- Expected: 1 row
```
### Runtime Regression Tests
```csharp
[Fact]
public async Task CrossTenantRead_ShouldFail_WithRlsError()
{
// Arrange: Insert data as tenant A
await using var connA = await OpenTenantConnectionAsync("tenant-a", ct);
await InsertFinding(connA, "finding-1", ct);
// Act: Try to read as tenant B
await using var connB = await OpenTenantConnectionAsync("tenant-b", ct);
var result = await QueryFindings(connB, ct);
// Assert: No rows returned (RLS blocks cross-tenant access)
Assert.Empty(result);
}
[Fact]
public async Task NoTenantContext_ShouldFail_WithException()
{
// Arrange: Open connection without setting tenant
await using var conn = await _dataSource.OpenConnectionAsync(ct);
// Act & Assert: Query should fail
await Assert.ThrowsAsync<PostgresException>(async () =>
{
await conn.ExecuteAsync("SELECT * FROM ledger_events LIMIT 1");
});
}
```
---
## Migration Strategy
### Migration File: `007_enable_rls.sql`
```sql
-- Migration: Enable Row-Level Security for Findings Ledger
-- Date: 2025-12-XX
-- Task: LEDGER-TEN-48-001-DEV
BEGIN;
-- 1. Create app schema and tenant function
CREATE SCHEMA IF NOT EXISTS findings_ledger_app;
CREATE OR REPLACE FUNCTION findings_ledger_app.require_current_tenant()
RETURNS TEXT LANGUAGE plpgsql STABLE AS $$
DECLARE tenant_text TEXT;
BEGIN
tenant_text := current_setting('app.current_tenant', true);
IF tenant_text IS NULL OR length(trim(tenant_text)) = 0 THEN
RAISE EXCEPTION 'app.current_tenant is not set' USING ERRCODE = 'P0001';
END IF;
RETURN tenant_text;
END;
$$;
-- 2. Enable RLS on all tables (see full SQL above)
-- ... (apply to all 8 tables)
-- 3. Create admin bypass role
CREATE ROLE IF NOT EXISTS findings_ledger_admin NOLOGIN BYPASSRLS;
COMMIT;
```
### Rollback: `007_enable_rls_rollback.sql`
```sql
BEGIN;
-- Disable RLS on all tables
ALTER TABLE ledger_events DISABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_merkle_roots DISABLE ROW LEVEL SECURITY;
ALTER TABLE findings_projection DISABLE ROW LEVEL SECURITY;
ALTER TABLE finding_history DISABLE ROW LEVEL SECURITY;
ALTER TABLE triage_actions DISABLE ROW LEVEL SECURITY;
ALTER TABLE ledger_attestations DISABLE ROW LEVEL SECURITY;
ALTER TABLE orchestrator_exports DISABLE ROW LEVEL SECURITY;
ALTER TABLE airgap_imports DISABLE ROW LEVEL SECURITY;
-- Drop policies
DROP POLICY IF EXISTS ledger_events_tenant_isolation ON ledger_events;
DROP POLICY IF EXISTS ledger_merkle_roots_tenant_isolation ON ledger_merkle_roots;
DROP POLICY IF EXISTS findings_projection_tenant_isolation ON findings_projection;
DROP POLICY IF EXISTS finding_history_tenant_isolation ON finding_history;
DROP POLICY IF EXISTS triage_actions_tenant_isolation ON triage_actions;
DROP POLICY IF EXISTS ledger_attestations_tenant_isolation ON ledger_attestations;
DROP POLICY IF EXISTS orchestrator_exports_tenant_isolation ON orchestrator_exports;
DROP POLICY IF EXISTS airgap_imports_tenant_isolation ON airgap_imports;
-- Drop function and schema
DROP FUNCTION IF EXISTS findings_ledger_app.require_current_tenant();
DROP SCHEMA IF EXISTS findings_ledger_app;
COMMIT;
```
---
## Audit Requirements
1. **All write operations** must log `tenant_id` and `actor_id`
2. **System connections** must log reason and operator
3. **RLS bypass operations** must be audited separately
4. **Cross-tenant queries** (admin only) must require justification ticket
---
## Reference Implementation
Evidence Locker RLS implementation:
- `src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/Db/Migrations/001_initial_schema.sql`
---
## Approval Checklist
- [ ] Platform/DB Guild: Schema and RLS patterns approved
- [ ] Security Guild: Tenant isolation verified
- [ ] Findings Ledger Guild: Implementation feasible
- [ ] DevOps Guild: Migration/rollback strategy approved
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-05 | Platform Guild | Initial contract based on Evidence Locker pattern |

View File

@@ -0,0 +1,212 @@
# Mirror Bundle Contract (AIRGAP-56)
**Contract ID:** `CONTRACT-MIRROR-BUNDLE-003`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the mirror bundle format used for air-gap/offline operation. Mirror bundles package VEX advisories, vulnerability feeds, and policy packs for transport to sealed environments.
## Implementation References
- **JSON Schema:** `docs/schemas/mirror-bundle.schema.json`
- **Documentation:** `docs/airgap/mirror-bundles.md`
- **Importer:** `src/AirGap/StellaOps.AirGap.Importer/`
## Bundle Structure
### MirrorBundle
Top-level bundle object.
```json
{
"schemaVersion": 1,
"generatedAt": "2025-12-05T10:00:00Z",
"targetRepository": "oci://registry.internal/stella/mirrors",
"domainId": "vex-advisories",
"displayName": "VEX Advisories",
"exports": [
{ ... }
]
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `schemaVersion` | integer | Yes | Bundle schema version (currently 1) |
| `generatedAt` | datetime | Yes | ISO-8601 generation timestamp |
| `targetRepository` | string | No | Target OCI repository |
| `domainId` | string | Yes | Domain identifier |
| `displayName` | string | No | Human-readable name |
| `exports` | array | Yes | Exported data sets |
### BundleExport
Individual export within a bundle.
```json
{
"key": "vex-openvex-all",
"format": "openvex",
"exportId": "550e8400-e29b-41d4-a716-446655440000",
"querySignature": "abc123def456",
"createdAt": "2025-12-05T10:00:00Z",
"artifactSizeBytes": 1048576,
"artifactDigest": "sha256:7d9cd5f1a2a0dd9a41a2c43a5b7d8a0bcd9e34cf39b3f43a70595c834f0a4aee",
"sourceProviders": ["anchore", "github", "redhat"],
"consensusRevision": "rev-2025-12-05-001",
"policyRevisionId": "policy-v1.2.3",
"policyDigest": "sha256:...",
"consensusDigest": "sha256:...",
"scoreDigest": "sha256:...",
"attestation": {
"predicateType": "https://stella.ops/attestation/vex-export/v1",
"signedAt": "2025-12-05T10:00:01Z",
"envelopeDigest": "sha256:...",
"rekorLocation": "https://rekor.sigstore.dev/api/v1/log/entries/..."
}
}
```
### Export Formats
| Format | Description |
|--------|-------------|
| `openvex` | OpenVEX format |
| `csaf` | CSAF VEX format |
| `cyclonedx` | CycloneDX VEX format |
| `spdx` | SPDX format |
| `ndjson` | Newline-delimited JSON |
| `json` | Standard JSON |
### AttestationDescriptor
Attestation metadata for signed exports.
```json
{
"predicateType": "https://stella.ops/attestation/vex-export/v1",
"rekorLocation": "https://rekor.sigstore.dev/...",
"envelopeDigest": "sha256:...",
"signedAt": "2025-12-05T10:00:01Z"
}
```
### BundleSignature
Signature for bundle integrity.
```json
{
"path": "bundle.sig",
"algorithm": "ES256",
"keyId": "key-2025-001",
"provider": "default",
"signedAt": "2025-12-05T10:00:02Z"
}
```
## Domain IDs
Standard domain identifiers:
| Domain ID | Description |
|-----------|-------------|
| `vex-advisories` | VEX advisory documents |
| `vulnerability-feeds` | Vulnerability feed data |
| `policy-packs` | Policy rule packages |
| `sbom-catalog` | SBOM artifacts |
## Validation Requirements
### DSSE Verification
1. Validate DSSE envelope structure
2. Verify RSA-PSS/SHA256 signature
3. Check trusted key fingerprint
4. Validate PAE encoding
### TUF Validation
1. Verify root → snapshot → timestamp chain
2. Check version monotonicity
3. Validate expiry windows
4. Cross-reference hashes
### Merkle Root Verification
1. Compute SHA-256 tree for bundle objects
2. Compare against stored Merkle root
3. Validate staged content integrity
## Import Flow
```
1. Receive bundle package
2. Validate DSSE signature
3. Verify TUF metadata chain
4. Compute and verify Merkle root
5. Register in bundle catalog
6. Apply to sealed environment
```
## Registration API
### Register Bundle
```
POST /api/v1/airgap/bundles
Content-Type: application/json
{
"bundlePath": "/path/to/bundle.json",
"trustRootsPath": "/path/to/trust-roots.json"
}
Response: 202 Accepted
{
"importId": "...",
"status": "validating"
}
```
### Get Bundle Status
```
GET /api/v1/airgap/bundles/{bundleId}
Response: 200 OK
{
"bundleId": "...",
"domainId": "vex-advisories",
"status": "imported",
"exportCount": 3
}
```
## Determinism Guarantees
1. **Digest verification:** All artifacts verified by SHA-256 digest
2. **Stable ordering:** Exports ordered deterministically
3. **Immutable content:** Bundle content is immutable once signed
4. **Traceability:** Full provenance chain via attestations
## Unblocks
This contract unblocks the following tasks:
- POLICY-AIRGAP-56-001
- POLICY-AIRGAP-56-002
- EXCITITOR-AIRGAP-56-001
- EXCITITOR-AIRGAP-58-001
- CLI-AIRGAP-56-001
- AIRGAP-TIME-57-001
## Related Contracts
- [Sealed Mode Contract](./sealed-mode.md) - Sealed environment operation
- [Verification Policy Contract](./verification-policy.md) - Attestation verification
- [Export Bundle Contract](./export-bundle.md) - Export job scheduling

View File

@@ -0,0 +1,335 @@
# Policy Studio API Contract
**Contract ID:** `CONTRACT-POLICY-STUDIO-007`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the Policy Studio API used for creating, editing, and managing security policies. Policy Studio extends the Policy Engine REST API with DSL compilation and draft management capabilities.
## Implementation References
- **Policy Engine:** `src/Policy/StellaOps.Policy.Engine/`
- **Policy API:** `src/Api/StellaOps.Api.OpenApi/policy/openapi.yaml`
- **Documentation:** `docs/api/policy.md`
## Policy Lifecycle
```
Draft → Submitted → Approved → Active → Archived
```
| State | Description |
|-------|-------------|
| `draft` | Policy is being edited, not enforced |
| `submitted` | Policy submitted for review |
| `approved` | Policy approved, ready to activate |
| `active` | Policy is currently enforced |
| `archived` | Policy is no longer active |
## API Endpoints
### Draft Management
#### Create Draft
```
POST /api/v1/policy/drafts
Content-Type: application/json
Authorization: Bearer <token>
{
"tenant_id": "default",
"name": "security-policy-v2",
"description": "Enhanced security policy with KEV checks",
"source_format": "stelladsl",
"source": "package policy\n\ndefault allow := false\n\nallow if {\n input.severity != \"critical\"\n}"
}
Response: 201 Created
{
"draft_id": "draft-001",
"name": "security-policy-v2",
"state": "draft",
"created_at": "2025-12-05T10:00:00Z",
"created_by": "user@example.com"
}
```
#### Get Draft
```
GET /api/v1/policy/drafts/{draft_id}
Response: 200 OK
{
"draft_id": "draft-001",
"name": "security-policy-v2",
"description": "Enhanced security policy with KEV checks",
"state": "draft",
"source_format": "stelladsl",
"source": "...",
"compiled_rego": "...",
"validation_errors": [],
"created_at": "2025-12-05T10:00:00Z",
"updated_at": "2025-12-05T10:00:00Z"
}
```
#### Update Draft
```
PUT /api/v1/policy/drafts/{draft_id}
Content-Type: application/json
{
"source": "updated policy source..."
}
Response: 200 OK
```
#### Delete Draft
```
DELETE /api/v1/policy/drafts/{draft_id}
Response: 204 No Content
```
### DSL Compilation
#### Compile DSL to Rego
```
POST /api/v1/policy/dsl/compile
Content-Type: application/json
{
"source": "package policy\n\ndefault allow := false\n\nallow if { input.severity != \"critical\" }",
"format": "stelladsl"
}
Response: 200 OK
{
"rego": "package policy\n\ndefault allow := false\n\nallow = true {\n input.severity != \"critical\"\n}",
"errors": [],
"warnings": [
{
"line": 5,
"column": 1,
"message": "Consider adding documentation comment"
}
]
}
```
#### Validate Policy
```
POST /api/v1/policy/dsl/validate
Content-Type: application/json
{
"source": "...",
"format": "stelladsl"
}
Response: 200 OK
{
"valid": true,
"errors": [],
"warnings": []
}
```
### Submission & Approval
#### Submit Draft for Review
```
POST /api/v1/policy/drafts/{draft_id}/submit
Content-Type: application/json
{
"comment": "Ready for review"
}
Response: 200 OK
{
"draft_id": "draft-001",
"state": "submitted",
"submitted_at": "2025-12-05T10:00:00Z",
"submitted_by": "user@example.com"
}
```
#### Approve Policy
```
POST /api/v1/policy/drafts/{draft_id}/approve
Authorization: Bearer <token with policy:approve scope>
{
"comment": "Approved after review"
}
Response: 200 OK
{
"draft_id": "draft-001",
"state": "approved",
"approved_at": "2025-12-05T10:00:00Z",
"approved_by": "admin@example.com"
}
```
#### Activate Policy
```
POST /api/v1/policy/drafts/{draft_id}/activate
Authorization: Bearer <token with policy:activate scope>
Response: 200 OK
{
"policy_id": "policy-001",
"version": "1.0.0",
"state": "active",
"activated_at": "2025-12-05T10:00:00Z"
}
```
### Policy Versions
#### List Policy Versions
```
GET /api/v1/policy/{policy_id}/versions
Response: 200 OK
{
"versions": [
{
"version": "1.0.0",
"state": "active",
"activated_at": "2025-12-05T10:00:00Z"
},
{
"version": "0.9.0",
"state": "archived",
"archived_at": "2025-12-05T09:00:00Z"
}
]
}
```
#### Get Specific Version
```
GET /api/v1/policy/{policy_id}/versions/{version}
Response: 200 OK
{
"policy_id": "policy-001",
"version": "1.0.0",
"rego": "...",
"hash": "sha256:...",
"state": "active"
}
```
## Policy Evaluation
#### Evaluate Policy
```
POST /api/v1/policy/{policy_id}/evaluate
Content-Type: application/json
{
"input": {
"finding_id": "finding-001",
"severity": "high",
"cvss": 7.5,
"kev": true
}
}
Response: 200 OK
{
"result": {
"allow": false,
"deny": true,
"reasons": ["KEV vulnerability detected"]
},
"policy_version": "1.0.0",
"policy_hash": "sha256:...",
"evaluated_at": "2025-12-05T10:00:00Z"
}
```
## DSL Format
### StellaOps DSL (stelladsl)
```rego
package policy
import future.keywords.if
import future.keywords.in
# Default deny
default allow := false
default deny := false
# Allow low severity findings
allow if {
input.severity in ["low", "informational"]
}
# Deny KEV vulnerabilities
deny if {
input.kev == true
}
# Deny critical CVSS
deny if {
input.cvss >= 9.0
}
```
## Error Codes
| Code | Message |
|------|---------|
| `ERR_POL_001` | Invalid policy syntax |
| `ERR_POL_002` | Compilation failed |
| `ERR_POL_003` | Validation failed |
| `ERR_POL_004` | Policy not found |
| `ERR_POL_005` | Invalid state transition |
| `ERR_POL_006` | Insufficient permissions |
## Authority Scopes
| Scope | Description |
|-------|-------------|
| `policy:read` | Read policies and drafts |
| `policy:write` | Create and edit drafts |
| `policy:submit` | Submit drafts for review |
| `policy:approve` | Approve submitted policies |
| `policy:activate` | Activate approved policies |
| `policy:archive` | Archive active policies |
## Unblocks
This contract unblocks the following tasks:
- CONCELIER-RISK-68-001
- POLICY-RISK-68-001
- POLICY-RISK-68-002
## Related Contracts
- [Risk Scoring Contract](./risk-scoring.md) - Policy affects scoring
- [Authority Effective Write Contract](./authority-effective-write.md) - Policy attachment

View File

@@ -0,0 +1,414 @@
# CONTRACT-RICHGRAPH-V1-015: Reachability Graph Schema
> **Status:** Published
> **Version:** 1.0.0
> **Published:** 2025-12-05
> **Owners:** Scanner Guild, Signals Guild, BE-Base Platform Guild
> **Unblocks:** GRAPH-CAS-401-001, GAP-SYM-007, SCAN-REACH-401-009, SCANNER-NATIVE-401-015, SYMS-SERVER-401-011, SYMS-CLIENT-401-012, SYMS-INGEST-401-013, SIGNALS-RUNTIME-401-002, GAP-REP-004, and 40+ downstream tasks
## Overview
This contract defines the canonical `richgraph-v1` schema used for function-level reachability analysis, CAS storage, and DSSE attestation. It specifies the data model, hash algorithms, determinism rules, and CAS layout enabling provable reachability claims.
---
## Schema Definition
### richgraph-v1 Document Structure
```json
{
"schema": "richgraph-v1",
"analyzer": {
"name": "scanner.reachability",
"version": "0.1.0",
"toolchain_digest": "sha256:..."
},
"nodes": [
{
"id": "sym:java:base64url...",
"symbol_id": "sym:java:base64url...",
"lang": "java",
"kind": "method",
"display": "com.example.Foo.bar(String)",
"code_id": "code:java:base64url...",
"purl": "pkg:maven/com.example/foo@1.0.0",
"build_id": "gnu-build-id:...",
"symbol_digest": "sha256:...",
"evidence": ["import", "disasm"],
"attributes": {"key": "value"}
}
],
"edges": [
{
"from": "sym:java:...",
"to": "sym:java:...",
"kind": "call",
"purl": "pkg:maven/com.example/bar@2.0.0",
"symbol_digest": "sha256:...",
"confidence": 0.9,
"evidence": ["reloc", "runtime"],
"candidates": []
}
],
"roots": [
{
"id": "sym:java:...",
"phase": "runtime",
"source": "main"
}
]
}
```
### Node Schema
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Unique node identifier (typically same as `symbol_id`) |
| `symbol_id` | string | Yes | Canonical SymbolID (format: `sym:{lang}:{base64url-sha256}`) |
| `lang` | string | Yes | Language: `java`, `dotnet`, `go`, `node`, `rust`, `python`, `ruby`, `php`, `binary`, `shell` |
| `kind` | string | Yes | Symbol kind: `method`, `function`, `class`, `module`, `trait`, `struct` |
| `display` | string | No | Human-readable demangled name |
| `code_id` | string | No | CodeID for name-less symbols (format: `code:{lang}:{base64url-sha256}`) |
| `purl` | string | No | Package URL of containing package |
| `build_id` | string | No | GNU build-id, PE GUID, or Mach-O UUID |
| `symbol_digest` | string | No | SHA-256 of the symbol_id (format: `sha256:{hex}`) |
| `evidence` | string[] | No | Evidence sources (sorted): `import`, `reloc`, `disasm`, `runtime` |
| `attributes` | object | No | Additional key-value metadata (sorted by key) |
### Edge Schema
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `from` | string | Yes | Source node ID |
| `to` | string | Yes | Target node ID |
| `kind` | string | Yes | Edge type: `call`, `virtual`, `indirect`, `data`, `init` |
| `purl` | string | No | Package URL of callee |
| `symbol_digest` | string | No | SHA-256 of callee symbol_id |
| `confidence` | number | Yes | Confidence [0.0-1.0]: `certain`=1.0, `high`=0.9, `medium`=0.6, `low`=0.3 |
| `evidence` | string[] | No | Evidence sources (sorted) |
| `candidates` | string[] | No | Alternative resolution candidates (sorted) |
### Root Schema
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | Yes | Node ID designated as entry point |
| `phase` | string | Yes | Execution phase: `runtime`, `load`, `init`, `test` |
| `source` | string | No | Entry point source (e.g., `main`, `DT_INIT`, `.ctors`) |
---
## Hash Algorithms
### Summary
| Component | Algorithm | Format | Example |
|-----------|-----------|--------|---------|
| **graph_hash** | BLAKE3-256 | `blake3:{hex}` | `blake3:a1b2c3d4...` |
| **symbol_digest** | SHA-256 | `sha256:{hex}` | `sha256:e5f6a7b8...` |
| **symbol_id fragment** | SHA-256 | base64url-no-pad | `sym:java:abc123...` |
| **code_id fragment** | SHA-256 | base64url-no-pad | `code:java:xyz789...` |
### Graph Hash (BLAKE3-256)
The graph hash provides content-addressable identification:
```
graph_hash = "blake3:" + hex(BLAKE3-256(canonical_json_bytes))
```
**Rationale:** BLAKE3 chosen for:
- Speed (3x+ faster than SHA-256 on modern CPUs)
- Parallelizable for large graphs
- Cryptographic security equivalent to SHA-256
- Consistent with internal content-addressing standard
### Symbol Digest (SHA-256)
Symbol digests use SHA-256 for interoperability:
```
symbol_digest = "sha256:" + hex(SHA-256(utf8(symbol_id)))
```
### SymbolID and CodeID Fragments
Internal fragments use SHA-256 with base64url encoding:
```
fragment = base64url_no_pad(SHA-256(utf8(canonical_tuple)))
symbol_id = "sym:{lang}:{fragment}"
code_id = "code:{lang}:{fragment}"
```
---
## Determinism Rules
All outputs must be reproducible. The `Trimmed()` operation enforces canonical ordering:
### Ordering Rules
1. **Nodes:** Sort by `id` (ordinal string comparison)
2. **Edges:** Sort by `(from, to, kind)` in that order (ordinal)
3. **Roots:** Sort by `id` (ordinal)
4. **Evidence arrays:** Sort alphabetically (ordinal)
5. **Candidates arrays:** Sort alphabetically (ordinal)
6. **Attributes objects:** Sort keys alphabetically (ordinal)
### Normalization Rules
1. **Trim whitespace:** All string values trimmed
2. **Empty to null:** Empty strings become null/omitted
3. **Confidence clamping:** Values clamped to [0.0, 1.0]
4. **Default values:**
- `kind` defaults to `"call"` for edges
- `phase` defaults to `"runtime"` for roots
- `analyzer.name` defaults to `"scanner.reachability"`
- `analyzer.version` defaults to `"0.1.0"`
### JSON Serialization
- No indentation (compact JSON)
- Keys sorted alphabetically at all levels
- No trailing whitespace
- UTF-8 encoding
- No BOM
---
## CAS Layout
### Graph Storage
```
cas://reachability/graphs/{blake3} # Graph body (canonical JSON)
cas://reachability/graphs/{blake3}.dsse # DSSE envelope
```
### Edge Bundle Storage (Optional)
For runtime hits, init-array roots, and contested edges:
```
cas://reachability/edges/{graph_hash}/{bundle_id} # Edge bundle body
cas://reachability/edges/{graph_hash}/{bundle_id}.dsse # DSSE envelope
```
### Metadata Storage
```
{output_root}/reachability_graphs/{analysis_id}/richgraph-v1.json # Graph body
{output_root}/reachability_graphs/{analysis_id}/meta.json # Metadata
```
**meta.json structure:**
```json
{
"schema": "richgraph-v1",
"graph_hash": "blake3:...",
"files": [
{"path": "...", "hash": "blake3:..."}
]
}
```
---
## DSSE Integration
### Predicate Types
| Predicate | Purpose |
|-----------|---------|
| `stella.ops/graph@v1` | Graph-level attestation |
| `stella.ops/edgeBundle@v1` | Edge bundle attestation |
### Graph DSSE (Mandatory)
Every richgraph-v1 document requires a DSSE envelope:
```json
{
"payloadType": "application/vnd.stellaops.graph+json",
"payload": "<base64(canonical_graph_json)>",
"signatures": [...]
}
```
**Subject:** `cas://reachability/graphs/{blake3}`
### Rekor Integration
- **Graph DSSE:** Always publish to Rekor (or mirror when offline)
- **Edge Bundle DSSE:** Optional, capped at configurable limit per graph
---
## SymbolID Construction
### Format
```
sym:{lang}:{base64url_sha256_no_pad}
```
### Per-Language Canonical Tuples
| Language | Tuple Components (NUL-separated) |
|----------|----------------------------------|
| Java | `{package}\0{class}\0{method}\0{descriptor}` (lowercased) |
| .NET | `{assembly}\0{namespace}\0{type}\0{member_signature}` |
| Go | `{module}\0{package}\0{receiver}\0{func}` |
| Node/Deno | `{pkg_or_path}\0{export_path}\0{kind}` |
| Rust | `{crate}\0{module}\0{item}\0{mangled?}` |
| Python | `{pkg_or_path}\0{module}\0{qualified_name}` |
| Ruby | `{gem_or_path}\0{module}\0{method}` |
| PHP | `{composer_pkg}\0{namespace}\0{qualified_name}` |
| Binary | `{file_hash}\0{section}\0{addr}\0{name}\0{linkage}\0{code_block_hash?}` |
| Shell | `{script_rel_path}\0{function_or_cmd}` |
| Swift | `{module}\0{type}\0{member}\0{mangled?}` |
---
## CodeID Construction
### Format
```
code:{lang}:{base64url_sha256_no_pad}
```
### Use Cases
CodeIDs provide stable identifiers when symbol names are unavailable:
- **Stripped binaries:** `code:binary:{hash}` from `{format}\0{file_hash}\0{addr}\0{length}\0{section}\0{code_block_hash}`
- **.NET modules:** `code:dotnet:{hash}` from `{assembly}\0{module}\0{mvid}`
- **Node packages:** `code:node:{hash}` from `{package}\0{entry_path}`
---
## Implementation Status
### Existing Implementation
| Component | Location | Status |
|-----------|----------|--------|
| RichGraph model | `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraph.cs` | Implemented |
| SymbolId builder | `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SymbolId.cs` | Implemented |
| CodeId builder | `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/CodeId.cs` | Implemented |
| RichGraphWriter | `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/RichGraphWriter.cs` | **Needs BLAKE3** |
| DSSE predicates | `src/Signer/StellaOps.Signer/PredicateTypes.cs` | Implemented |
### Required Changes
| Change | Priority | Notes |
|--------|----------|-------|
| Update RichGraphWriter to use BLAKE3 | P0 | Currently uses SHA256 for graph_hash |
| Add `meta.json` hash prefix | P1 | Use `blake3:` prefix |
| CAS adapter for graph storage | P1 | Implement `cas://reachability/graphs/{blake3}` paths |
---
## Decision Checklist
This contract resolves the following decisions from the 2025-12-02 alignment meeting:
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Graph hash algorithm | BLAKE3-256 | Speed + security |
| Symbol digest algorithm | SHA-256 | Interoperability |
| CAS path scheme | `cas://reachability/graphs/{blake3}` | Content-addressable |
| DSSE required for graphs | Yes (mandatory) | Provenance chain |
| DSSE for edge bundles | Optional (capped) | Rekor volume control |
| JSON canonicalization | Sorted keys, compact | Determinism |
| Hash prefix format | `{alg}:{hex}` | Explicit algorithm ID |
---
## Validation Rules
### Schema Validation
1. `schema` must equal `"richgraph-v1"`
2. `nodes` array must not be empty
3. All node `id` values must be unique
4. All edge `from`/`to` must reference existing nodes
5. All root `id` values must reference existing nodes
6. `confidence` must be in range [0.0, 1.0]
### Hash Validation
1. `graph_hash` must match BLAKE3-256 of canonical JSON
2. `symbol_digest` must match SHA-256 of `symbol_id`
3. SymbolID fragments must match SHA-256 of canonical tuple
---
## Migration Path
### From Current Implementation
1. **RichGraphWriter:** Replace `ComputeSha256` with `ComputeBlake3` for graph hash
2. **meta.json:** Update hash format from `sha256:` to `blake3:`
3. **Existing graphs:** Recompute hashes on next scan (no migration needed)
### Compatibility
- Symbol digests remain SHA-256 (no change)
- SymbolID format unchanged
- CodeID format unchanged
---
## Reference Implementation
### Canonical JSON Writer
```csharp
// From RichGraph.cs - Trimmed() enforces canonical ordering
public RichGraph Trimmed()
{
var nodes = Nodes.OrderBy(n => n.Id, StringComparer.Ordinal).ToList();
var edges = Edges
.OrderBy(e => e.From, StringComparer.Ordinal)
.ThenBy(e => e.To, StringComparer.Ordinal)
.ThenBy(e => e.Kind, StringComparer.Ordinal)
.ToList();
var roots = Roots.OrderBy(r => r.Id, StringComparer.Ordinal).ToList();
return this with { Nodes = nodes, Edges = edges, Roots = roots };
}
```
### BLAKE3 Graph Hash (Required Update)
```csharp
// Replace in RichGraphWriter.cs
private static string ComputeBlake3(byte[] bytes)
{
using var blake3 = Blake3.Hasher.New();
blake3.Update(bytes);
var hash = blake3.Finalize();
return "blake3:" + Convert.ToHexString(hash.AsSpan()).ToLowerInvariant();
}
```
---
## Related Contracts
- [Sealed Mode](./sealed-mode.md) - Air-gap operation with CAS
- [Mirror Bundle](./mirror-bundle.md) - Offline transport format
- [Verification Policy](./verification-policy.md) - DSSE verification rules
- [Scanner Surface](./scanner-surface.md) - Surface analysis framework
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-05 | Scanner Guild | Initial contract from alignment meeting |

View File

@@ -0,0 +1,286 @@
# Risk Scoring Contract (66-002)
**Contract ID:** `CONTRACT-RISK-SCORING-002`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the risk scoring interface used by the Policy Engine to calculate and prioritize vulnerability findings. It covers job requests, results, risk profiles, and signal definitions.
## Implementation References
- **Scoring Models:** `src/Policy/StellaOps.Policy.Engine/Scoring/RiskScoringModels.cs`
- **Risk Profile:** `src/Policy/StellaOps.Policy.RiskProfile/Models/RiskProfileModel.cs`
- **Attestation Schema:** `src/Attestor/StellaOps.Attestor.Types/schemas/stellaops-risk-profile.v1.schema.json`
## Data Models
### RiskScoringJobRequest
Request to create a risk scoring job.
```json
{
"tenant_id": "string",
"context_id": "string",
"profile_id": "string",
"findings": [
{
"finding_id": "string",
"component_purl": "pkg:npm/lodash@4.17.20",
"advisory_id": "CVE-2024-1234",
"trigger": "created|updated|enriched|vex_applied"
}
],
"priority": "low|normal|high|emergency",
"correlation_id": "string (optional)",
"requested_at": "2025-12-05T00:00:00Z (optional)"
}
```
### RiskScoringJob
A queued or completed risk scoring job.
```json
{
"job_id": "string",
"tenant_id": "string",
"context_id": "string",
"profile_id": "string",
"profile_hash": "sha256:...",
"findings": [...],
"priority": "normal",
"status": "queued|running|completed|failed|cancelled",
"requested_at": "2025-12-05T00:00:00Z",
"started_at": "2025-12-05T00:00:01Z (optional)",
"completed_at": "2025-12-05T00:00:02Z (optional)",
"correlation_id": "string (optional)",
"error_message": "string (optional)"
}
```
### RiskScoringResult
Result of scoring a single finding.
```json
{
"finding_id": "string",
"profile_id": "string",
"profile_version": "1.0.0",
"raw_score": 0.75,
"normalized_score": 0.85,
"severity": "high",
"signal_values": {
"cvss": 7.5,
"kev": true,
"reachability": 0.9
},
"signal_contributions": {
"cvss": 0.4,
"kev": 0.3,
"reachability": 0.3
},
"override_applied": "kev-boost (optional)",
"override_reason": "Known Exploited Vulnerability (optional)",
"scored_at": "2025-12-05T00:00:02Z"
}
```
## Risk Profile Model
### RiskProfileModel
Defines how findings are scored and prioritized.
```json
{
"id": "default-profile",
"version": "1.0.0",
"description": "Default risk profile for vulnerability prioritization",
"extends": "base-profile (optional)",
"signals": [
{
"name": "cvss",
"source": "nvd",
"type": "numeric",
"path": "/cvss/base_score",
"transform": "normalize_10",
"unit": "score"
},
{
"name": "kev",
"source": "cisa",
"type": "boolean",
"path": "/kev/in_catalog"
},
{
"name": "reachability",
"source": "scanner",
"type": "numeric",
"path": "/reachability/score"
}
],
"weights": {
"cvss": 0.4,
"kev": 0.3,
"reachability": 0.3
},
"overrides": {
"severity": [
{
"when": { "kev": true },
"set": "critical"
}
],
"decisions": [
{
"when": { "kev": true, "reachability": { "$gt": 0.8 } },
"action": "deny",
"reason": "KEV with high reachability"
}
]
},
"metadata": {}
}
```
### Signal Types
| Type | Description | Value Range |
|------|-------------|-------------|
| `boolean` | True/false signal | `true` / `false` |
| `numeric` | Numeric signal | `0.0` to `1.0` (normalized) |
| `categorical` | Categorical signal | String values |
### Severity Levels
| Level | JSON Value | Priority |
|-------|------------|----------|
| Critical | `"critical"` | 1 (highest) |
| High | `"high"` | 2 |
| Medium | `"medium"` | 3 |
| Low | `"low"` | 4 |
| Informational | `"informational"` | 5 (lowest) |
### Decision Actions
| Action | Description |
|--------|-------------|
| `allow` | Finding is acceptable, no action required |
| `review` | Finding requires manual review |
| `deny` | Finding is not acceptable, blocks promotion |
## Scoring Algorithm
### Score Calculation
```
raw_score = Σ(signal_value × weight) for all signals
normalized_score = clamp(raw_score, 0.0, 1.0)
```
### VEX Gate Provider
The VEX gate provider short-circuits scoring when a VEX denial is present:
```csharp
if (signals.HasVexDenial)
return 0.0; // Fully mitigated
return Math.Max(signals.Values); // Otherwise, max signal
```
### CVSS + KEV Provider
```csharp
score = clamp01((cvss / 10.0) + kevBonus)
where kevBonus = kev ? 0.2 : 0.0
```
## API Endpoints
### Submit Scoring Job
```
POST /api/v1/risk/jobs
Content-Type: application/json
{
"tenant_id": "...",
"context_id": "...",
"profile_id": "...",
"findings": [...]
}
Response: 202 Accepted
{
"job_id": "...",
"status": "queued"
}
```
### Get Job Status
```
GET /api/v1/risk/jobs/{job_id}
Response: 200 OK
{
"job_id": "...",
"status": "completed",
"results": [...]
}
```
### Get Finding Score
```
GET /api/v1/risk/findings/{finding_id}/score
Response: 200 OK
{
"finding_id": "...",
"normalized_score": 0.85,
"severity": "high",
...
}
```
## Finding Change Events
Events that trigger rescoring:
| Event | JSON Value | Description |
|-------|------------|-------------|
| Created | `"created"` | New finding discovered |
| Updated | `"updated"` | Finding metadata changed |
| Enriched | `"enriched"` | New signals available |
| VEX Applied | `"vex_applied"` | VEX status changed |
## Determinism Guarantees
1. **Reproducible scores:** Same inputs always produce same outputs
2. **Profile versioning:** Profile hash included in results for traceability
3. **Signal ordering:** Signals processed in deterministic order
4. **Timestamp precision:** UTC ISO-8601 with millisecond precision
## Unblocks
This contract unblocks the following tasks:
- LEDGER-RISK-67-001
- LEDGER-RISK-68-001
- LEDGER-RISK-69-001
- POLICY-RISK-67-003
- POLICY-RISK-68-001
- POLICY-RISK-68-002
## Related Contracts
- [Advisory Key Contract](./advisory-key.md) - Advisory ID canonicalization
- [VEX Lens Contract](./vex-lens.md) - VEX evidence for scoring
- [Export Bundle Contract](./export-bundle.md) - Score digest in exports

View File

@@ -0,0 +1,346 @@
# CONTRACT-SCANNER-PHP-ANALYZER-013: PHP Language Analyzer Bootstrap
> **Status:** Published
> **Version:** 1.0.0
> **Published:** 2025-12-05
> **Owners:** PHP Analyzer Guild, Scanner Guild
> **Unblocks:** SCANNER-ANALYZERS-PHP-27-001
## Overview
This contract defines the PHP language analyzer bootstrap specification, including composer manifest parsing, VFS (Virtual File System) schema, and offline kit target requirements for deterministic PHP project analysis.
## Scope
The PHP analyzer will:
1. Parse `composer.json` and `composer.lock` files
2. Build virtual file system from source trees, vendor directories, and configs
3. Detect framework/CMS fingerprints (Laravel, Symfony, WordPress, Drupal, etc.)
4. Emit SBOM components with PHP-specific PURLs
5. Support offline analysis via cached dependencies
---
## Input Normalization
### Source Tree Merge
The analyzer merges these sources into a unified VFS:
```
Priority (highest to lowest):
1. /app (mounted application source)
2. /vendor (composer dependencies)
3. /etc/php* (PHP configuration)
4. Container layer filesystem
```
### File Discovery
```csharp
public interface IPhpSourceDiscovery
{
IAsyncEnumerable<PhpSourceFile> DiscoverAsync(
string rootPath,
PhpDiscoveryOptions options,
CancellationToken ct);
}
public record PhpDiscoveryOptions
{
public bool IncludeVendor { get; init; } = true;
public bool IncludeTests { get; init; } = false;
public string[] ExcludePatterns { get; init; } = ["*.min.php", "cache/*"];
}
```
---
## Composer Schema
### composer.json Parsing
```json
{
"name": "vendor/package",
"version": "1.2.3",
"type": "library|project|metapackage|composer-plugin",
"require": {
"php": ">=8.1",
"vendor/dependency": "^2.0"
},
"require-dev": { },
"autoload": {
"psr-4": { "App\\": "src/" },
"classmap": ["database/"],
"files": ["helpers.php"]
}
}
```
### composer.lock Parsing
Extract exact versions and content hashes:
```csharp
public record ComposerLockPackage
{
public string Name { get; init; }
public string Version { get; init; }
public string Source { get; init; } // type: git|hg|svn
public string Dist { get; init; } // type: zip|tar
public string Reference { get; init; } // commit hash or tag
public string ContentHash { get; init; } // SHA256 of package contents
}
```
### PURL Format
```
pkg:composer/vendor/package@version
pkg:composer/laravel/framework@10.0.0
pkg:composer/symfony/http-kernel@6.3.0
```
---
## VFS Schema
### Virtual File System Model
```csharp
public record PhpVirtualFileSystem
{
public string RootPath { get; init; }
public IReadOnlyList<VfsEntry> Entries { get; init; }
public PhpConfiguration PhpConfig { get; init; }
public ComposerManifest Composer { get; init; }
public string ContentHash { get; init; } // BLAKE3 of sorted entries
}
public record VfsEntry
{
public string RelativePath { get; init; }
public VfsEntryType Type { get; init; }
public long Size { get; init; }
public string ContentHash { get; init; }
public DateTimeOffset ModifiedAt { get; init; }
}
public enum VfsEntryType
{
PhpSource,
PhpConfig,
ComposerJson,
ComposerLock,
Vendor,
Asset,
Config
}
```
### Deterministic Ordering
VFS entries MUST be sorted by:
1. `RelativePath` (case-sensitive, lexicographic)
2. Stable hash computation across runs
---
## Framework Detection
### Fingerprint Rules
| Framework | Detection Method | Confidence |
|-----------|-----------------|------------|
| Laravel | `artisan` file + `Illuminate\` namespace | High |
| Symfony | `symfony.lock` or `config/bundles.php` | High |
| WordPress | `wp-config.php` + `wp-includes/` | High |
| Drupal | `core/lib/Drupal.php` | High |
| Magento | `app/etc/env.php` + `Magento\` namespace | High |
| CodeIgniter | `system/core/CodeIgniter.php` | Medium |
| Yii | `yii` or `yii2` in composer | Medium |
| CakePHP | `cakephp/cakephp` in composer | Medium |
### Fingerprint Output
```csharp
public record PhpFrameworkFingerprint
{
public string Name { get; init; }
public string Version { get; init; }
public ConfidenceLevel Confidence { get; init; }
public IReadOnlyList<string> IndicatorFiles { get; init; }
}
```
---
## PHP Configuration
### Config File Discovery
```
/etc/php/*/php.ini
/etc/php/*/conf.d/*.ini
/etc/php-fpm.d/*.conf
/usr/local/etc/php/php.ini (Alpine)
```
### Security-Relevant Settings
Extract and report:
```csharp
public record PhpSecurityConfig
{
public bool AllowUrlFopen { get; init; }
public bool AllowUrlInclude { get; init; }
public string OpenBasedir { get; init; }
public string DisableFunctions { get; init; }
public string DisableClasses { get; init; }
public bool ExposePhp { get; init; }
public bool DisplayErrors { get; init; }
}
```
---
## Output Schema
### SBOM Component
```json
{
"type": "library",
"bom-ref": "pkg:composer/vendor/package@1.2.3",
"purl": "pkg:composer/vendor/package@1.2.3",
"name": "vendor/package",
"version": "1.2.3",
"properties": [
{ "name": "stellaops:php:framework", "value": "laravel" },
{ "name": "stellaops:php:phpVersion", "value": ">=8.1" },
{ "name": "stellaops:php:autoload", "value": "psr-4" }
],
"evidence": {
"identity": {
"field": "purl",
"confidence": 1.0,
"methods": [{ "technique": "manifest-analysis", "value": "composer.lock" }]
}
}
}
```
### Analysis Store Keys
```csharp
public static class PhpAnalysisKeys
{
public const string VirtualFileSystem = "php.vfs";
public const string ComposerManifest = "php.composer";
public const string FrameworkFingerprints = "php.frameworks";
public const string SecurityConfig = "php.security-config";
public const string Autoload = "php.autoload";
}
```
---
## Offline Kit Target
### Bundle Structure
```
offline/php-analyzer/
├── manifests/
│ └── php-analyzer-manifest.json
├── fixtures/
│ ├── laravel-app/
│ ├── symfony-app/
│ └── wordpress-site/
├── vendor-cache/
│ └── packages.json (Packagist mirror index)
└── SHA256SUMS
```
### Air-Gap Operation
```csharp
public interface IOfflineComposerRepository
{
Task<ComposerPackage?> ResolveAsync(string name, string version, CancellationToken ct);
Task<bool> IsAvailableOfflineAsync(string name, string version, CancellationToken ct);
}
```
---
## Test Fixtures
### Required Fixtures
| Fixture | Purpose |
|---------|---------|
| `laravel-10-app/` | Laravel 10.x application with mix/vite |
| `symfony-6-app/` | Symfony 6.x with Doctrine |
| `wordpress-6-site/` | WordPress 6.x with plugins |
| `drupal-10-site/` | Drupal 10.x with modules |
| `composer-only/` | Pure library project |
| `legacy-php56/` | PHP 5.6 compatibility test |
### Golden Output Format
```
fixtures/<name>/
├── composer.json
├── composer.lock
├── src/
├── EXPECTED.sbom.json # Expected SBOM output
├── EXPECTED.vfs.json # Expected VFS structure
└── EXPECTED.meta.json # Expected fingerprints
```
---
## Implementation Path
### Phase 1: Core Parser
1. Implement `ComposerJsonParser`
2. Implement `ComposerLockParser`
3. Add PURL generation
4. Basic VFS construction
### Phase 2: Framework Detection
1. Implement fingerprint rules
2. Add confidence scoring
3. Version detection
### Phase 3: Offline Support
1. Implement vendor cache
2. Add offline repository
3. Bundle generation
---
## Project Location
```
src/Scanner/StellaOps.Scanner.Analyzers.Lang.Php/
├── StellaOps.Scanner.Analyzers.Lang.Php.csproj
├── ComposerJsonParser.cs
├── ComposerLockParser.cs
├── PhpVirtualFileSystem.cs
├── PhpFrameworkDetector.cs
├── PhpLanguageAnalyzer.cs
├── PhpAnalyzerPlugin.cs
└── README.md
```
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-05 | PHP Analyzer Guild | Initial contract |

View File

@@ -0,0 +1,282 @@
# CONTRACT-SCANNER-SURFACE-014: Scanner Surface Analysis Framework
> **Status:** Published
> **Version:** 1.0.0
> **Published:** 2025-12-05
> **Owners:** Scanner Guild
> **Unblocks:** SCANNER-SURFACE-01
## Overview
This contract defines the Scanner Surface analysis framework scope, providing the task definition and contract required for implementing comprehensive attack surface analysis across scanner modules.
## Scope
SCANNER-SURFACE-01 establishes the foundational surface analysis patterns that integrate:
- Entry point discovery across language analyzers
- Attack surface enumeration and classification
- Policy signal emission for surface findings
- Integration with Surface.FS, Surface.Env, and Surface.Secrets
---
## Surface Analysis Model
### Surface Types
| Type | Description | Detection Method |
|------|-------------|------------------|
| Network | Exposed ports, listeners, endpoints | EntryTrace, config analysis |
| File | Sensitive file access, path traversal | VFS analysis, permission checks |
| Process | Command execution, subprocess spawn | Call graph, runtime trace |
| Crypto | Key/secret handling, weak algorithms | Pattern matching, API usage |
| Auth | Authentication bypass, session handling | Framework detection, config |
| Input | User input handling, injection points | Data flow analysis |
### Surface Entry
```csharp
public record SurfaceEntry
{
public string Id { get; init; } // SHA256(type|path|context)
public SurfaceType Type { get; init; }
public string Path { get; init; } // File path or endpoint
public string Context { get; init; } // Function/method context
public ConfidenceLevel Confidence { get; init; }
public IReadOnlyList<string> Tags { get; init; }
public SurfaceEvidence Evidence { get; init; }
}
public enum SurfaceType
{
NetworkEndpoint,
FileOperation,
ProcessExecution,
CryptoOperation,
AuthenticationPoint,
InputHandling,
SecretAccess,
ExternalCall
}
```
---
## Integration Points
### Surface.FS Integration
```csharp
public interface ISurfaceManifestWriter
{
Task WriteSurfaceEntriesAsync(
string scanId,
IEnumerable<SurfaceEntry> entries,
CancellationToken ct);
}
```
### Surface.Env Integration
Environment configuration for surface analysis:
```
STELLA_SURFACE_ENABLED=true
STELLA_SURFACE_DEPTH=3 # Call graph depth
STELLA_SURFACE_CONFIDENCE=0.7 # Minimum confidence threshold
STELLA_SURFACE_CACHE_ROOT=/var/cache/stella/surface
```
### Surface.Secrets Integration
```csharp
public interface ISurfaceSecretScanner
{
IAsyncEnumerable<SecretFinding> ScanAsync(
IPhysicalFileProvider files,
SecretScanOptions options,
CancellationToken ct);
}
```
---
## Policy Signals
### Surface Signal Keys
```csharp
public static class SurfaceSignalKeys
{
public const string NetworkEndpoints = "surface.network.endpoints";
public const string ExposedPorts = "surface.network.ports";
public const string FileOperations = "surface.file.operations";
public const string ProcessSpawns = "surface.process.spawns";
public const string CryptoUsage = "surface.crypto.usage";
public const string AuthPoints = "surface.auth.points";
public const string InputHandlers = "surface.input.handlers";
public const string SecretAccess = "surface.secrets.access";
public const string TotalSurfaceArea = "surface.total.area";
}
```
### Signal Emission
```csharp
public interface ISurfaceSignalEmitter
{
Task EmitAsync(
string scanId,
IDictionary<string, object> signals,
CancellationToken ct);
}
```
---
## Entry Point Discovery
### Language Analyzer Integration
Each language analyzer contributes surface entries:
| Analyzer | Entry Points |
|----------|--------------|
| .NET | Controllers, Minimal APIs, SignalR hubs |
| Java | Servlets, JAX-RS resources, Spring MVC |
| Node | Express routes, Fastify handlers |
| Python | Flask/Django views, FastAPI endpoints |
| Go | HTTP handlers, gRPC services |
| PHP | Routes, controller actions |
| Deno | HTTP handlers, permissions |
### Entry Point Model
```csharp
public record EntryPoint
{
public string Id { get; init; }
public string Language { get; init; }
public string Framework { get; init; }
public string Path { get; init; } // URL path or route
public string Method { get; init; } // HTTP method or RPC
public string Handler { get; init; } // Function/method name
public string File { get; init; }
public int Line { get; init; }
public IReadOnlyList<string> Parameters { get; init; }
public IReadOnlyList<string> Middlewares { get; init; }
}
```
---
## Output Schema
### Surface Analysis Result
```json
{
"scanId": "scan-abc123",
"timestamp": "2025-12-05T12:00:00Z",
"summary": {
"totalEntries": 42,
"byType": {
"NetworkEndpoint": 15,
"FileOperation": 10,
"ProcessExecution": 5,
"CryptoOperation": 8,
"SecretAccess": 4
},
"riskScore": 0.65
},
"entries": [
{
"id": "sha256:...",
"type": "NetworkEndpoint",
"path": "/api/users",
"context": "UserController.GetUsers",
"confidence": 0.95,
"evidence": {
"file": "src/Controllers/UserController.cs",
"line": 42,
"hash": "sha256:..."
}
}
]
}
```
### Analysis Store Key
```csharp
public const string SurfaceAnalysisKey = "scanner.surface.analysis";
```
---
## Determinism Requirements
1. **Stable IDs:** Entry IDs computed as `SHA256(type|path|context)`
2. **Sorted Output:** Entries sorted by ID
3. **Reproducible Hashes:** Content hashes use BLAKE3
4. **Canonical JSON:** Output serialized with sorted keys
---
## Implementation Phases
### Phase 1: Core Framework
- [ ] Define `SurfaceEntry` model
- [ ] Implement entry point collector registry
- [ ] Add Surface.FS manifest writer integration
- [ ] Basic policy signal emission
### Phase 2: Language Integration
- [ ] Wire .NET entry point discovery
- [ ] Wire Java entry point discovery
- [ ] Wire Node entry point discovery
- [ ] Wire Python entry point discovery
### Phase 3: Advanced Analysis
- [ ] Data flow tracking
- [ ] Secret pattern detection
- [ ] Crypto usage analysis
- [ ] Attack path enumeration
---
## Project Structure
```
src/Scanner/__Libraries/StellaOps.Scanner.Surface/
├── StellaOps.Scanner.Surface.csproj
├── Models/
│ ├── SurfaceEntry.cs
│ ├── SurfaceType.cs
│ └── EntryPoint.cs
├── Discovery/
│ ├── ISurfaceEntryCollector.cs
│ └── SurfaceEntryRegistry.cs
├── Signals/
│ └── SurfaceSignalEmitter.cs
├── Output/
│ └── SurfaceAnalysisWriter.cs
└── README.md
```
---
## Dependencies
- `StellaOps.Scanner.Surface.FS` - Manifest storage
- `StellaOps.Scanner.Surface.Env` - Environment configuration
- `StellaOps.Scanner.Surface.Secrets` - Secret detection
- `StellaOps.Scanner.EntryTrace` - Entry point tracing
---
## Changelog
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0.0 | 2025-12-05 | Scanner Guild | Initial contract |

View File

@@ -0,0 +1,300 @@
# Sealed Mode Contract (AIRGAP-57)
**Contract ID:** `CONTRACT-SEALED-MODE-004`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the sealed-mode operation contract for air-gapped environments. It covers sealing/unsealing state transitions, staleness detection, time anchoring, and egress policy enforcement.
## Implementation References
- **Controller:** `src/AirGap/StellaOps.AirGap.Controller/`
- **Time:** `src/AirGap/StellaOps.AirGap.Time/`
- **Policy:** `src/AirGap/StellaOps.AirGap.Policy/`
- **Documentation:** `docs/airgap/sealing-and-egress.md`, `docs/airgap/staleness-and-time.md`
## Data Models
### AirGapState
The core sealed-mode state model.
```csharp
public sealed record AirGapState
{
public string Id { get; init; } = "singleton";
public string TenantId { get; init; } = "default";
public bool Sealed { get; init; } = false;
public string? PolicyHash { get; init; } = null;
public TimeAnchor TimeAnchor { get; init; } = TimeAnchor.Unknown;
public DateTimeOffset LastTransitionAt { get; init; }
public StalenessBudget StalenessBudget { get; init; } = StalenessBudget.Default;
}
```
### JSON Representation
```json
{
"id": "singleton",
"tenant_id": "default",
"sealed": true,
"policy_hash": "sha256:...",
"time_anchor": {
"anchor_time": "2025-12-05T10:00:00Z",
"source": "roughtime",
"format": "roughtime",
"signature_fingerprint": "...",
"token_digest": "sha256:..."
},
"last_transition_at": "2025-12-05T10:00:00Z",
"staleness_budget": {
"warning_seconds": 3600,
"breach_seconds": 7200
}
}
```
### TimeAnchor
Cryptographically verified time reference.
```json
{
"anchor_time": "2025-12-05T10:00:00Z",
"source": "roughtime|rfc3161",
"format": "roughtime|rfc3161",
"signature_fingerprint": "sha256:...",
"token_digest": "sha256:..."
}
```
### StalenessBudget
Defines staleness thresholds.
```json
{
"warning_seconds": 3600,
"breach_seconds": 7200
}
```
| Field | Default | Description |
|-------|---------|-------------|
| `warning_seconds` | 3600 | Warning threshold (1 hour) |
| `breach_seconds` | 7200 | Breach threshold (2 hours) |
### StalenessEvaluation
Result of staleness calculation.
```json
{
"age_seconds": 1800,
"warning_seconds": 3600,
"breach_seconds": 7200,
"is_breached": false,
"remaining_seconds": 1800
}
```
## API Endpoints
### Seal Environment
```
POST /system/airgap/seal
Content-Type: application/json
Authorization: Bearer <token with airgap:seal scope>
{
"policy_hash": "sha256:...",
"time_anchor": { ... },
"staleness_budget": {
"warning_seconds": 3600,
"breach_seconds": 7200
}
}
Response: 200 OK
{
"sealed": true,
"last_transition_at": "2025-12-05T10:00:00Z"
}
```
### Unseal Environment
```
POST /system/airgap/unseal
Authorization: Bearer <token with airgap:seal scope>
Response: 200 OK
{
"sealed": false,
"last_transition_at": "2025-12-05T10:00:00Z"
}
```
### Get Status
```
GET /system/airgap/status
Authorization: Bearer <token with airgap:status:read scope>
Response: 200 OK
{
"sealed": true,
"tenant_id": "default",
"staleness": {
"age_seconds": 1800,
"is_breached": false,
"remaining_seconds": 1800
},
"time_anchor": { ... },
"policy_hash": "sha256:..."
}
```
### Verify Bundle
```
POST /system/airgap/verify
Content-Type: application/json
Authorization: Bearer <token with airgap:verify scope>
{
"bundle_path": "/path/to/bundle.json",
"trust_roots_path": "/path/to/trust-roots.json"
}
Response: 200 OK
{
"valid": true,
"verification_result": {
"dsse_valid": true,
"tuf_valid": true,
"merkle_valid": true
}
}
```
## Egress Policy
### EgressPolicy Model
```csharp
public sealed class EgressPolicy
{
public EgressPolicyMode Mode { get; } // Sealed | Unsealed
public IReadOnlyList<string> AllowedHosts { get; }
public bool PermitLoopback { get; }
public bool PermitPrivateNetworks { get; }
}
```
### EgressRequest / EgressDecision
```json
// Request
{
"component": "excititor",
"destination": "https://api.github.com",
"intent": "fetch_advisories",
"operation": "GET"
}
// Decision
{
"allowed": false,
"reason": "AIRGAP_EGRESS_BLOCKED",
"remediation": "Add api.github.com to allowlist or unseal environment"
}
```
### Enforcement
When sealed:
- All outbound connections blocked by default
- Only allowlisted destinations permitted
- Loopback and private networks optionally permitted
## Time Verification
### Roughtime Verification
1. Parse Roughtime response
2. Verify Ed25519 signature against trusted public key
3. Extract anchor time from signed response
### RFC 3161 Verification
1. Parse SignedCms structure
2. Validate TSA certificate chain
3. Extract signing time from timestamp token
## Startup Diagnostics
Pre-flight checks when starting in sealed mode:
1. Verify time anchor is present
2. Check staleness budget not breached
3. Validate trust roots are loaded
4. Confirm egress policy is enforced
```
GET /healthz/ready
Response: 200 OK (if healthy)
Response: 503 Service Unavailable (if sealed mode requirements unmet)
```
## Telemetry
### Metrics
| Metric | Type | Description |
|--------|------|-------------|
| `airgap_sealed` | gauge | 1 if sealed, 0 if unsealed |
| `airgap_anchor_drift_seconds` | gauge | Seconds since time anchor |
| `airgap_anchor_expiry_seconds` | gauge | Seconds until staleness breach |
| `airgap_seal_total` | counter | Total seal operations |
| `airgap_unseal_total` | counter | Total unseal operations |
| `airgap_startup_blocked_total` | counter | Blocked startup attempts |
### Structured Logging
```json
{
"event": "airgap.sealed",
"tenant_id": "default",
"policy_hash": "sha256:...",
"timestamp": "2025-12-05T10:00:00Z"
}
```
## Authority Scopes
| Scope | Description |
|-------|-------------|
| `airgap:seal` | Seal/unseal environment |
| `airgap:status:read` | Read sealed status |
| `airgap:verify` | Verify bundles |
| `airgap:import` | Import bundles |
## Unblocks
This contract unblocks the following tasks:
- POLICY-AIRGAP-57-001
- POLICY-AIRGAP-57-002
- POLICY-AIRGAP-58-001
## Related Contracts
- [Mirror Bundle Contract](./mirror-bundle.md) - Bundle format for sealed import
- [Verification Policy Contract](./verification-policy.md) - Attestation verification

View File

@@ -0,0 +1,298 @@
# Verification Policy Contract
**Contract ID:** `CONTRACT-VERIFICATION-POLICY-006`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the VerificationPolicy schema used to configure attestation verification requirements. It specifies which predicate types are allowed, signer requirements, and tenant-scoped verification rules.
## Implementation References
- **Predicate Types:** `src/Signer/StellaOps.Signer/StellaOps.Signer.Core/PredicateTypes.cs`
- **Attestor Core:** `src/Attestor/StellaOps.Attestor/StellaOps.Attestor.Core/Verification/`
- **Schema:** `src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json`
## JSON Schema
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://stellaops.io/schemas/verification-policy.v1.json",
"title": "VerificationPolicy",
"description": "Attestation verification policy configuration",
"type": "object",
"required": ["policyId", "version", "predicateTypes", "signerRequirements"],
"properties": {
"policyId": {
"type": "string",
"description": "Unique policy identifier",
"pattern": "^[a-z0-9-]+$"
},
"version": {
"type": "string",
"description": "Policy version (SemVer)",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
},
"description": {
"type": "string",
"description": "Human-readable policy description"
},
"tenantScope": {
"type": "string",
"description": "Tenant ID this policy applies to, or '*' for all tenants"
},
"predicateTypes": {
"type": "array",
"description": "Allowed attestation predicate types",
"items": {
"type": "string"
},
"minItems": 1
},
"signerRequirements": {
"$ref": "#/$defs/SignerRequirements"
},
"validityWindow": {
"$ref": "#/$defs/ValidityWindow"
},
"metadata": {
"type": "object",
"additionalProperties": true
}
},
"$defs": {
"SignerRequirements": {
"type": "object",
"properties": {
"minimumSignatures": {
"type": "integer",
"minimum": 1,
"default": 1,
"description": "Minimum number of valid signatures required"
},
"trustedKeyFingerprints": {
"type": "array",
"items": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"description": "List of trusted signer key fingerprints"
},
"trustedIssuers": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of trusted issuer identities"
},
"requireRekor": {
"type": "boolean",
"default": false,
"description": "Require Rekor transparency log entry"
},
"algorithms": {
"type": "array",
"items": {
"type": "string",
"enum": ["ES256", "ES384", "ES512", "RS256", "RS384", "RS512", "EdDSA"]
},
"description": "Allowed signing algorithms"
}
}
},
"ValidityWindow": {
"type": "object",
"properties": {
"notBefore": {
"type": "string",
"format": "date-time",
"description": "Policy not valid before this time"
},
"notAfter": {
"type": "string",
"format": "date-time",
"description": "Policy not valid after this time"
},
"maxAttestationAge": {
"type": "integer",
"minimum": 0,
"description": "Maximum age of attestation in seconds"
}
}
}
}
}
```
## Example Policy
```json
{
"policyId": "default-verification-policy",
"version": "1.0.0",
"description": "Default verification policy for StellaOps attestations",
"tenantScope": "*",
"predicateTypes": [
"stella.ops/sbom@v1",
"stella.ops/vex@v1",
"stella.ops/vexDecision@v1",
"stella.ops/policy@v1",
"stella.ops/promotion@v1",
"stella.ops/evidence@v1",
"stella.ops/graph@v1",
"stella.ops/replay@v1",
"https://slsa.dev/provenance/v1",
"https://cyclonedx.org/bom",
"https://spdx.dev/Document",
"https://openvex.dev/ns"
],
"signerRequirements": {
"minimumSignatures": 1,
"trustedKeyFingerprints": [
"sha256:abc123...",
"sha256:def456..."
],
"requireRekor": false,
"algorithms": ["ES256", "RS256", "EdDSA"]
},
"validityWindow": {
"maxAttestationAge": 86400
}
}
```
## Predicate Types
### StellaOps Types
| Type URI | Description |
|----------|-------------|
| `stella.ops/promotion@v1` | Promotion attestation |
| `stella.ops/sbom@v1` | SBOM attestation |
| `stella.ops/vex@v1` | VEX attestation |
| `stella.ops/vexDecision@v1` | VEX decision with reachability |
| `stella.ops/replay@v1` | Replay manifest attestation |
| `stella.ops/policy@v1` | Policy evaluation result |
| `stella.ops/evidence@v1` | Evidence chain |
| `stella.ops/graph@v1` | Graph/reachability attestation |
### Third-Party Types
| Type URI | Description |
|----------|-------------|
| `https://slsa.dev/provenance/v0.2` | SLSA Provenance v0.2 |
| `https://slsa.dev/provenance/v1` | SLSA Provenance v1.0 |
| `https://cyclonedx.org/bom` | CycloneDX SBOM |
| `https://spdx.dev/Document` | SPDX SBOM |
| `https://openvex.dev/ns` | OpenVEX |
## Verification Flow
```
1. Parse DSSE envelope
2. Extract predicate type from in-toto statement
3. Check predicate type against policy.predicateTypes
4. Verify signature(s) meet policy.signerRequirements
a. Check algorithm is allowed
b. Verify minimum signature count
c. Check key fingerprints against trusted list
5. If requireRekor, verify Rekor log entry
6. Check attestation timestamp against validityWindow
7. Return verification result
```
## API Endpoints
### Create Policy
```
POST /api/v1/attestor/policies
Content-Type: application/json
{
"policyId": "custom-policy",
"version": "1.0.0",
...
}
Response: 201 Created
```
### Get Policy
```
GET /api/v1/attestor/policies/{policyId}
Response: 200 OK
{ ... }
```
### Verify Attestation
```
POST /api/v1/attestor/verify
Content-Type: application/json
{
"envelope": "base64-encoded DSSE envelope",
"policyId": "default-verification-policy"
}
Response: 200 OK
{
"valid": true,
"predicateType": "stella.ops/sbom@v1",
"signatureCount": 1,
"signers": [
{
"keyFingerprint": "sha256:...",
"algorithm": "ES256",
"verified": true
}
],
"rekorEntry": null
}
```
## Verification Result
```json
{
"valid": true,
"predicateType": "stella.ops/sbom@v1",
"signatureCount": 1,
"signers": [
{
"keyFingerprint": "sha256:abc123...",
"issuer": "https://stellaops.io/signer",
"algorithm": "ES256",
"verified": true
}
],
"rekorEntry": {
"uuid": "24296fb24b8ad77a...",
"logIndex": 12345,
"integratedTime": "2025-12-05T10:00:00Z"
},
"attestationTimestamp": "2025-12-05T09:59:59Z",
"policyId": "default-verification-policy",
"policyVersion": "1.0.0"
}
```
## Unblocks
This contract unblocks the following tasks:
- POLICY-ATTEST-73-001
- POLICY-ATTEST-73-002
- POLICY-ATTEST-74-001
- POLICY-ATTEST-74-002
## Related Contracts
- [Mirror Bundle Contract](./mirror-bundle.md) - Uses verification for bundle import
- [Sealed Mode Contract](./sealed-mode.md) - Verification in air-gapped mode

317
docs/contracts/vex-lens.md Normal file
View File

@@ -0,0 +1,317 @@
# VEX Lens Contract
**Contract ID:** `CONTRACT-VEX-LENS-005`
**Version:** 1.0
**Status:** Published
**Last Updated:** 2025-12-05
## Overview
This contract defines the VEX Lens (VexLinkset) data model used to correlate multiple VEX observations for a specific vulnerability and product. The VEX Lens captures provider agreement, disagreements, and calculates consensus confidence.
## Implementation Reference
**Source:** `src/Excititor/__Libraries/StellaOps.Excititor.Core/Observations/VexLinkset.cs`
## Data Model
### VexLinkset
The core VEX Lens structure correlating observations.
```csharp
public sealed record VexLinkset
{
/// <summary>
/// Unique identifier: SHA256(tenant|vulnerabilityId|productKey)
/// </summary>
public string LinksetId { get; }
/// <summary>
/// Tenant identifier (normalized to lowercase).
/// </summary>
public string Tenant { get; }
/// <summary>
/// The vulnerability identifier (CVE, GHSA, vendor ID).
/// </summary>
public string VulnerabilityId { get; }
/// <summary>
/// Product key (typically a PURL or CPE).
/// </summary>
public string ProductKey { get; }
/// <summary>
/// Canonical scope metadata for the product key.
/// </summary>
public VexProductScope Scope { get; }
/// <summary>
/// References to observations that contribute to this linkset.
/// </summary>
public ImmutableArray<VexLinksetObservationRefModel> Observations { get; }
/// <summary>
/// Conflict annotations capturing disagreements between providers.
/// </summary>
public ImmutableArray<VexObservationDisagreement> Disagreements { get; }
/// <summary>
/// When this linkset was first created.
/// </summary>
public DateTimeOffset CreatedAt { get; }
/// <summary>
/// When this linkset was last updated.
/// </summary>
public DateTimeOffset UpdatedAt { get; }
}
```
### JSON Representation
```json
{
"linkset_id": "sha256:abc123...",
"tenant": "default",
"vulnerability_id": "CVE-2024-1234",
"product_key": "pkg:npm/lodash@4.17.20",
"scope": {
"ecosystem": "npm",
"namespace": null,
"name": "lodash",
"version": "4.17.20"
},
"observations": [
{
"observation_id": "obs-001",
"provider_id": "github",
"status": "affected",
"confidence": 0.9
},
{
"observation_id": "obs-002",
"provider_id": "redhat",
"status": "not_affected",
"confidence": 0.85
}
],
"disagreements": [
{
"provider_id": "github",
"status": "affected",
"justification": null,
"confidence": 0.9
},
{
"provider_id": "redhat",
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"confidence": 0.85
}
],
"created_at": "2025-12-05T10:00:00Z",
"updated_at": "2025-12-05T10:00:00Z"
}
```
### VexLinksetObservationRefModel
Reference to an observation contributing to the linkset.
```json
{
"observation_id": "obs-001",
"provider_id": "github",
"status": "affected",
"confidence": 0.9
}
```
| Field | Type | Description |
|-------|------|-------------|
| `observation_id` | string | Unique observation identifier |
| `provider_id` | string | VEX provider identifier |
| `status` | string | VEX status claim |
| `confidence` | double? | Optional confidence [0.0-1.0] |
### VexObservationDisagreement
Captures conflict between providers.
```json
{
"provider_id": "github",
"status": "affected",
"justification": null,
"confidence": 0.9
}
```
### VEX Status Values
| Status | Description |
|--------|-------------|
| `affected` | Product is affected by vulnerability |
| `not_affected` | Product is not affected |
| `fixed` | Vulnerability has been fixed |
| `under_investigation` | Status is being determined |
### VEX Justification Codes
When `status` is `not_affected`, justification may include:
| Code | Description |
|------|-------------|
| `component_not_present` | Vulnerable component not present |
| `vulnerable_code_not_present` | Vulnerable code not present |
| `vulnerable_code_not_in_execute_path` | Code present but not reachable |
| `vulnerable_code_cannot_be_controlled_by_adversary` | Not exploitable |
| `inline_mitigations_already_exist` | Mitigations in place |
## Confidence Levels
### VexLinksetConfidence
Computed confidence based on linkset state.
| Level | Conditions |
|-------|------------|
| `Low` | Conflicts exist, or < 1 observation, or multiple distinct statuses |
| `Medium` | Single provider, or consistent observations |
| `High` | 2+ providers agree on status |
### Confidence Calculation
```csharp
public VexLinksetConfidence Confidence
{
get
{
if (HasConflicts)
return VexLinksetConfidence.Low;
if (Observations.Length == 0)
return VexLinksetConfidence.Low;
if (Statuses.Count > 1)
return VexLinksetConfidence.Low;
if (ProviderIds.Count >= 2)
return VexLinksetConfidence.High;
return VexLinksetConfidence.Medium;
}
}
```
## Linkset ID Generation
Deterministic ID from key components:
```csharp
public static string CreateLinksetId(string tenant, string vulnerabilityId, string productKey)
{
var input = $"{tenant.ToLowerInvariant()}|{vulnerabilityId}|{productKey}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
```
## API Endpoints
### Resolve VEX for Finding
```
POST /excititor/resolve
Content-Type: application/json
{
"tenant_id": "default",
"queries": [
{
"vulnerability_id": "CVE-2024-1234",
"product_key": "pkg:npm/lodash@4.17.20"
}
]
}
Response: 200 OK
{
"results": [
{
"linkset_id": "sha256:...",
"vulnerability_id": "CVE-2024-1234",
"product_key": "pkg:npm/lodash@4.17.20",
"rollup_status": "affected",
"confidence": "medium",
"has_conflicts": false,
"provider_count": 1
}
]
}
```
### Get Linkset Details
```
GET /excititor/linksets/{linkset_id}
Response: 200 OK
{
"linkset_id": "sha256:...",
"vulnerability_id": "CVE-2024-1234",
"observations": [...],
"disagreements": [...],
"confidence": "low"
}
```
## Consensus Algorithm
The consensus rollup algorithm:
1. **Filter:** Remove invalid statements by signature policy
2. **Score:** `score = weight(provider) × freshnessFactor(lastObserved)`
3. **Aggregate:** `W(status) = Σ score` per status
4. **Pick:** `rollupStatus = argmax_status W(status)`
5. **Tie-breakers:**
- Higher max single provider score
- More recent `lastObserved`
- Lexicographic order (fixed > not_affected > under_investigation > affected)
### Provider Weights
| Provider Type | Default Weight |
|---------------|----------------|
| Vendor | 1.0 |
| Distribution | 0.9 |
| Platform | 0.7 |
| Attestation | 0.6 |
| Hub | 0.5 |
### Freshness Factor
```
freshnessFactor = clamp(0.8, 1.0 - (age_days / 30), 1.0)
```
## Determinism Guarantees
1. **Stable ID:** LinksetId is deterministic from (tenant, vulnId, productKey)
2. **Sorted observations:** Observations sorted by observationId
3. **Sorted disagreements:** Disagreements sorted by (providerId, status)
4. **Immutable records:** Linksets are immutable; updates create new versions
## Unblocks
This contract unblocks the following tasks:
- CONCELIER-VEXLENS-30-001
- EXCITITOR-VEXLENS-30-001
## Related Contracts
- [Advisory Key Contract](./advisory-key.md) - Vulnerability ID canonicalization
- [Risk Scoring Contract](./risk-scoring.md) - VEX evidence for scoring