feat: Add VEX Status Chip component and integration tests for reachability drift detection

- Introduced `VexStatusChipComponent` to display VEX status with color coding and tooltips.
- Implemented integration tests for reachability drift detection, covering various scenarios including drift detection, determinism, and error handling.
- Enhanced `ScannerToSignalsReachabilityTests` with a null implementation of `ICallGraphSyncService` for better test isolation.
- Updated project references to include the new Reachability Drift library.
This commit is contained in:
StellaOps Bot
2025-12-20 01:26:42 +02:00
parent edc91ea96f
commit 5fc469ad98
159 changed files with 41116 additions and 2305 deletions

View File

@@ -0,0 +1,305 @@
# Issuer Directory Contract v1.0.0
**Status:** APPROVED
**Version:** 1.0.0
**Effective:** 2025-12-19
**Owner:** VEX Lens Guild + Issuer Directory Guild
**Sprint:** SPRINT_0129_0001_0001 (unblocks VEXLENS-30-003)
---
## 1. Purpose
The Issuer Directory provides a registry of known VEX statement issuers with trust metadata, signing key information, and provenance tracking.
## 2. Data Model
### 2.1 Issuer Entity
```csharp
public sealed record Issuer
{
/// <summary>Unique issuer identifier (e.g., "vendor:redhat", "cert:cisa").</summary>
public required string IssuerId { get; init; }
/// <summary>Issuer category.</summary>
public required IssuerCategory Category { get; init; }
/// <summary>Display name.</summary>
public required string DisplayName { get; init; }
/// <summary>Trust tier assignment.</summary>
public required IssuerTrustTier TrustTier { get; init; }
/// <summary>Official website URL.</summary>
public string? WebsiteUrl { get; init; }
/// <summary>Security advisory feed URL.</summary>
public string? AdvisoryFeedUrl { get; init; }
/// <summary>Registered signing keys.</summary>
public ImmutableArray<SigningKeyInfo> SigningKeys { get; init; }
/// <summary>Products/ecosystems this issuer is authoritative for.</summary>
public ImmutableArray<string> AuthoritativeFor { get; init; }
/// <summary>When this issuer record was created.</summary>
public DateTimeOffset CreatedAt { get; init; }
/// <summary>When this issuer record was last updated.</summary>
public DateTimeOffset UpdatedAt { get; init; }
/// <summary>Whether issuer is active.</summary>
public bool IsActive { get; init; } = true;
}
```
### 2.2 Issuer Category
```csharp
public enum IssuerCategory
{
/// <summary>Software vendor/maintainer.</summary>
Vendor = 0,
/// <summary>Linux distribution.</summary>
Distribution = 1,
/// <summary>CERT/security response team.</summary>
Cert = 2,
/// <summary>Security research organization.</summary>
SecurityResearch = 3,
/// <summary>Community project.</summary>
Community = 4,
/// <summary>Commercial security vendor.</summary>
Commercial = 5
}
```
### 2.3 Signing Key Info
```csharp
public sealed record SigningKeyInfo
{
/// <summary>Key fingerprint (SHA-256).</summary>
public required string Fingerprint { get; init; }
/// <summary>Key type (pgp, x509, sigstore).</summary>
public required string KeyType { get; init; }
/// <summary>Key algorithm (rsa, ecdsa, ed25519).</summary>
public string? Algorithm { get; init; }
/// <summary>Key size in bits.</summary>
public int? KeySize { get; init; }
/// <summary>Key creation date.</summary>
public DateTimeOffset? CreatedAt { get; init; }
/// <summary>Key expiration date.</summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>Whether key is currently valid.</summary>
public bool IsValid { get; init; } = true;
/// <summary>Public key location (URL or inline).</summary>
public string? PublicKeyUri { get; init; }
}
```
## 3. Pre-Registered Issuers
### 3.1 Authoritative Tier (Trust Tier 0)
| Issuer ID | Display Name | Category | Authoritative For |
|-----------|--------------|----------|-------------------|
| `vendor:redhat` | Red Hat Product Security | Vendor | `pkg:rpm/redhat/*`, `pkg:oci/registry.redhat.io/*` |
| `vendor:canonical` | Ubuntu Security Team | Distribution | `pkg:deb/ubuntu/*` |
| `vendor:debian` | Debian Security Team | Distribution | `pkg:deb/debian/*` |
| `vendor:suse` | SUSE Security Team | Distribution | `pkg:rpm/suse/*`, `pkg:rpm/opensuse/*` |
| `vendor:microsoft` | Microsoft Security Response | Vendor | `pkg:nuget/*` (Microsoft packages) |
| `vendor:oracle` | Oracle Security | Vendor | `pkg:maven/com.oracle.*/*` |
| `vendor:apache` | Apache Security Team | Community | `pkg:maven/org.apache.*/*` |
| `vendor:google` | Google Security Team | Vendor | `pkg:golang/google.golang.org/*` |
### 3.2 Trusted Tier (Trust Tier 1)
| Issuer ID | Display Name | Category |
|-----------|--------------|----------|
| `cert:cisa` | CISA | Cert |
| `cert:nist` | NIST NVD | Cert |
| `cert:github` | GitHub Security Advisories | SecurityResearch |
| `cert:snyk` | Snyk Security | Commercial |
| `research:oss-fuzz` | Google OSS-Fuzz | SecurityResearch |
### 3.3 Community Tier (Trust Tier 2)
| Issuer ID | Display Name | Category |
|-----------|--------------|----------|
| `community:osv` | OSV (Open Source Vulnerabilities) | Community |
| `community:vulndb` | VulnDB | Community |
## 4. API Endpoints
### 4.1 List Issuers
```
GET /api/v1/issuers
```
Query Parameters:
- `category`: Filter by category
- `trust_tier`: Filter by trust tier
- `active`: Filter by active status (default: true)
- `limit`: Max results (default: 100)
- `cursor`: Pagination cursor
### 4.2 Get Issuer
```
GET /api/v1/issuers/{issuerId}
```
### 4.3 Register Issuer (Admin)
```
POST /api/v1/issuers
Authorization: Bearer {admin_token}
{
"issuerId": "vendor:acme",
"category": "vendor",
"displayName": "ACME Security",
"trustTier": "trusted",
"websiteUrl": "https://security.acme.example",
"advisoryFeedUrl": "https://security.acme.example/feed.json",
"authoritativeFor": ["pkg:npm/@acme/*"]
}
```
### 4.4 Register Signing Key (Admin)
```
POST /api/v1/issuers/{issuerId}/keys
Authorization: Bearer {admin_token}
{
"fingerprint": "sha256:abc123...",
"keyType": "pgp",
"algorithm": "rsa",
"keySize": 4096,
"publicKeyUri": "https://security.acme.example/keys/signing.asc"
}
```
### 4.5 Lookup by Fingerprint
```
GET /api/v1/issuers/by-fingerprint/{fingerprint}
```
Returns the issuer associated with a signing key fingerprint.
## 5. Trust Tier Resolution
### 5.1 Automatic Assignment
When a VEX statement is received:
1. **Check signature:** If signed, lookup issuer by key fingerprint
2. **Check domain:** Match issuer by advisory feed domain
3. **Check authoritativeFor:** Match issuer by product PURL patterns
4. **Fallback:** Assign `Unknown` tier if no match
### 5.2 Override Rules
Operators can configure trust overrides:
```yaml
# etc/vexlens.yaml
issuer_overrides:
- issuer_id: "community:custom-feed"
trust_tier: "trusted" # Promote community to trusted
- issuer_id: "vendor:untrusted-vendor"
trust_tier: "community" # Demote vendor to community
```
## 6. Issuer Verification
### 6.1 PGP Signature Verification
```csharp
public interface IIssuerVerifier
{
/// <summary>
/// Verifies a VEX document signature against registered issuer keys.
/// </summary>
Task<IssuerVerificationResult> VerifyAsync(
byte[] documentBytes,
byte[] signatureBytes,
CancellationToken cancellationToken = default);
}
public sealed record IssuerVerificationResult
{
public bool IsValid { get; init; }
public string? IssuerId { get; init; }
public string? KeyFingerprint { get; init; }
public IssuerTrustTier? TrustTier { get; init; }
public string? VerificationError { get; init; }
}
```
### 6.2 Sigstore Verification
For Sigstore-signed documents:
1. Verify Rekor inclusion proof
2. Extract OIDC identity from certificate
3. Match identity to registered issuer
4. Return issuer info with trust tier
## 7. Database Schema
```sql
CREATE TABLE vex.issuers (
issuer_id TEXT PRIMARY KEY,
category TEXT NOT NULL,
display_name TEXT NOT NULL,
trust_tier INT NOT NULL DEFAULT 3,
website_url TEXT,
advisory_feed_url TEXT,
authoritative_for TEXT[] DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE vex.issuer_signing_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
issuer_id TEXT NOT NULL REFERENCES vex.issuers(issuer_id),
fingerprint TEXT NOT NULL UNIQUE,
key_type TEXT NOT NULL,
algorithm TEXT,
key_size INT,
public_key_uri TEXT,
is_valid BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_issuer_signing_keys_fingerprint ON vex.issuer_signing_keys(fingerprint);
CREATE INDEX idx_issuers_trust_tier ON vex.issuers(trust_tier);
```
---
## Changelog
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2025-12-19 | Initial release |

View File

@@ -0,0 +1,271 @@
# VEX Normalization Contract v1.0.0
**Status:** APPROVED
**Version:** 1.0.0
**Effective:** 2025-12-19
**Owner:** VEX Lens Guild
**Sprint:** SPRINT_0129_0001_0001 (unblocks VEXLENS-30-001 through 30-011)
---
## 1. Purpose
This contract defines the normalization rules for VEX (Vulnerability Exploitability eXchange) documents from multiple sources into a canonical StellaOps internal representation.
## 2. Supported Input Formats
| Format | Version | Parser |
|--------|---------|--------|
| OpenVEX | 0.2.0+ | `OpenVexParser` |
| CycloneDX VEX | 1.5+ | `CycloneDxVexParser` |
| CSAF VEX | 2.0 | `CsafVexParser` |
## 3. Canonical Representation
### 3.1 NormalizedVexStatement
```csharp
public sealed record NormalizedVexStatement
{
/// <summary>Unique statement identifier (deterministic hash).</summary>
public required string StatementId { get; init; }
/// <summary>CVE or vulnerability identifier.</summary>
public required string VulnerabilityId { get; init; }
/// <summary>Normalized status (not_affected, affected, fixed, under_investigation).</summary>
public required VexStatus Status { get; init; }
/// <summary>Justification code (when status = not_affected).</summary>
public VexJustification? Justification { get; init; }
/// <summary>Human-readable impact statement.</summary>
public string? ImpactStatement { get; init; }
/// <summary>Action statement for remediation.</summary>
public string? ActionStatement { get; init; }
/// <summary>Products affected by this statement.</summary>
public required ImmutableArray<ProductIdentifier> Products { get; init; }
/// <summary>Source document metadata.</summary>
public required VexSourceMetadata Source { get; init; }
/// <summary>Statement timestamp (UTC, ISO-8601).</summary>
public required DateTimeOffset Timestamp { get; init; }
/// <summary>Issuer information.</summary>
public required IssuerInfo Issuer { get; init; }
}
```
### 3.2 VexStatus Enum
```csharp
public enum VexStatus
{
/// <summary>Product is not affected by the vulnerability.</summary>
NotAffected = 0,
/// <summary>Product is affected and vulnerable.</summary>
Affected = 1,
/// <summary>Product was affected but is now fixed.</summary>
Fixed = 2,
/// <summary>Impact is being investigated.</summary>
UnderInvestigation = 3
}
```
### 3.3 VexJustification Enum
```csharp
public enum VexJustification
{
/// <summary>Component is not present.</summary>
ComponentNotPresent = 0,
/// <summary>Vulnerable code is not present.</summary>
VulnerableCodeNotPresent = 1,
/// <summary>Vulnerable code is not in execute path.</summary>
VulnerableCodeNotInExecutePath = 2,
/// <summary>Vulnerable code cannot be controlled by adversary.</summary>
VulnerableCodeCannotBeControlledByAdversary = 3,
/// <summary>Inline mitigations exist.</summary>
InlineMitigationsAlreadyExist = 4
}
```
## 4. Normalization Rules
### 4.1 Status Mapping
| Source Format | Source Value | Normalized Status |
|---------------|--------------|-------------------|
| OpenVEX | `not_affected` | NotAffected |
| OpenVEX | `affected` | Affected |
| OpenVEX | `fixed` | Fixed |
| OpenVEX | `under_investigation` | UnderInvestigation |
| CycloneDX | `notAffected` | NotAffected |
| CycloneDX | `affected` | Affected |
| CycloneDX | `resolved` | Fixed |
| CycloneDX | `inTriage` | UnderInvestigation |
| CSAF | `not_affected` | NotAffected |
| CSAF | `known_affected` | Affected |
| CSAF | `fixed` | Fixed |
| CSAF | `under_investigation` | UnderInvestigation |
### 4.2 Justification Mapping
| Source Format | Source Value | Normalized Justification |
|---------------|--------------|--------------------------|
| OpenVEX | `component_not_present` | ComponentNotPresent |
| OpenVEX | `vulnerable_code_not_present` | VulnerableCodeNotPresent |
| OpenVEX | `vulnerable_code_not_in_execute_path` | VulnerableCodeNotInExecutePath |
| OpenVEX | `vulnerable_code_cannot_be_controlled_by_adversary` | VulnerableCodeCannotBeControlledByAdversary |
| OpenVEX | `inline_mitigations_already_exist` | InlineMitigationsAlreadyExist |
| CycloneDX | Same as OpenVEX (camelCase) | Same mapping |
| CSAF | `component_not_present` | ComponentNotPresent |
| CSAF | `vulnerable_code_not_present` | VulnerableCodeNotPresent |
| CSAF | `vulnerable_code_not_in_execute_path` | VulnerableCodeNotInExecutePath |
| CSAF | `vulnerable_code_cannot_be_controlled_by_adversary` | VulnerableCodeCannotBeControlledByAdversary |
| CSAF | `inline_mitigations_already_exist` | InlineMitigationsAlreadyExist |
### 4.3 Product Identifier Normalization
Products are normalized to PURL (Package URL) format:
```
pkg:{ecosystem}/{namespace}/{name}@{version}?{qualifiers}#{subpath}
```
| Source | Extraction Method |
|--------|-------------------|
| OpenVEX | Direct from `product.id` if PURL, else construct from `product.identifiers` |
| CycloneDX | From `bom-ref` PURL or construct from `component.purl` |
| CSAF | From `product_id``product_identification_helper.purl` |
### 4.4 Statement ID Generation
Statement IDs are deterministic SHA-256 hashes:
```csharp
public static string GenerateStatementId(
string vulnerabilityId,
VexStatus status,
IEnumerable<string> productPurls,
string issuerId,
DateTimeOffset timestamp)
{
var input = $"{vulnerabilityId}|{status}|{string.Join(",", productPurls.OrderBy(p => p))}|{issuerId}|{timestamp:O}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return $"stmt:{Convert.ToHexString(hash).ToLowerInvariant()[..32]}";
}
```
## 5. Issuer Directory Integration
Normalized statements include issuer information from the Issuer Directory:
```csharp
public sealed record IssuerInfo
{
/// <summary>Issuer identifier (e.g., "vendor:redhat", "vendor:canonical").</summary>
public required string IssuerId { get; init; }
/// <summary>Display name.</summary>
public required string DisplayName { get; init; }
/// <summary>Trust tier (authoritative, trusted, community, unknown).</summary>
public required IssuerTrustTier TrustTier { get; init; }
/// <summary>Issuer's signing key fingerprints (if signed).</summary>
public ImmutableArray<string> SigningKeyFingerprints { get; init; }
}
public enum IssuerTrustTier
{
Authoritative = 0, // Vendor/maintainer of the product
Trusted = 1, // Known security research org
Community = 2, // Community contributor
Unknown = 3 // Unverified source
}
```
## 6. API Governance
### 6.1 Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/vex/statements` | GET | Query normalized statements |
| `/api/v1/vex/statements/{id}` | GET | Get specific statement |
| `/api/v1/vex/normalize` | POST | Normalize a VEX document |
| `/api/v1/vex/issuers` | GET | List known issuers |
| `/api/v1/vex/issuers/{id}` | GET | Get issuer details |
### 6.2 Query Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `vulnerability` | string | Filter by CVE/vulnerability ID |
| `product` | string | Filter by PURL (URL-encoded) |
| `status` | enum | Filter by VEX status |
| `issuer` | string | Filter by issuer ID |
| `since` | datetime | Statements after timestamp |
| `limit` | int | Max results (default: 100, max: 1000) |
| `cursor` | string | Pagination cursor |
### 6.3 Response Format
```json
{
"statements": [
{
"statementId": "stmt:a1b2c3d4e5f6...",
"vulnerabilityId": "CVE-2024-1234",
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"products": ["pkg:npm/lodash@4.17.21"],
"issuer": {
"issuerId": "vendor:lodash",
"displayName": "Lodash Maintainers",
"trustTier": "authoritative"
},
"timestamp": "2024-12-19T10:30:00Z"
}
],
"cursor": "next_page_token",
"total": 42
}
```
## 7. Precedence Rules
When multiple statements exist for the same vulnerability+product:
1. **Timestamp:** Later statements supersede earlier ones
2. **Trust Tier:** Higher trust tiers take precedence (Authoritative > Trusted > Community > Unknown)
3. **Specificity:** More specific product matches win (exact version > version range > package)
## 8. Validation
All normalized statements must pass:
1. `vulnerabilityId` matches CVE/GHSA/vendor pattern
2. `status` is a valid enum value
3. `products` contains at least one valid PURL
4. `timestamp` is valid ISO-8601 UTC
5. `issuer.issuerId` exists in Issuer Directory or is marked Unknown
---
## Changelog
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2025-12-19 | Initial release |