Add unit tests for RabbitMq and Udp transport servers and clients
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
106
docs/contracts/README.md
Normal file
106
docs/contracts/README.md
Normal 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
|
||||
186
docs/contracts/advisory-key.md
Normal file
186
docs/contracts/advisory-key.md
Normal 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
|
||||
292
docs/contracts/api-governance-baseline.md
Normal file
292
docs/contracts/api-governance-baseline.md
Normal 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 |
|
||||
271
docs/contracts/authority-effective-write.md
Normal file
271
docs/contracts/authority-effective-write.md
Normal 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
|
||||
294
docs/contracts/crypto-provider-registry.md
Normal file
294
docs/contracts/crypto-provider-registry.md
Normal 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
|
||||
324
docs/contracts/export-bundle.md
Normal file
324
docs/contracts/export-bundle.md
Normal 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
|
||||
416
docs/contracts/findings-ledger-rls.md
Normal file
416
docs/contracts/findings-ledger-rls.md
Normal 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 |
|
||||
212
docs/contracts/mirror-bundle.md
Normal file
212
docs/contracts/mirror-bundle.md
Normal 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
|
||||
335
docs/contracts/policy-studio.md
Normal file
335
docs/contracts/policy-studio.md
Normal 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
|
||||
414
docs/contracts/richgraph-v1.md
Normal file
414
docs/contracts/richgraph-v1.md
Normal 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 |
|
||||
286
docs/contracts/risk-scoring.md
Normal file
286
docs/contracts/risk-scoring.md
Normal 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
|
||||
346
docs/contracts/scanner-php-analyzer.md
Normal file
346
docs/contracts/scanner-php-analyzer.md
Normal 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 |
|
||||
282
docs/contracts/scanner-surface.md
Normal file
282
docs/contracts/scanner-surface.md
Normal 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 |
|
||||
300
docs/contracts/sealed-mode.md
Normal file
300
docs/contracts/sealed-mode.md
Normal 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
|
||||
298
docs/contracts/verification-policy.md
Normal file
298
docs/contracts/verification-policy.md
Normal 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
317
docs/contracts/vex-lens.md
Normal 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
|
||||
Reference in New Issue
Block a user