up
This commit is contained in:
@@ -1,91 +1,168 @@
|
||||
# Excititor VEX linkset APIs (observations + linksets)
|
||||
# Excititor VEX Observation & Linkset APIs
|
||||
|
||||
> Draft examples for Sprint 119 (EXCITITOR-LNM-21-203). Aligns with WebService endpoints implemented in `src/Excititor/StellaOps.Excititor.WebService/Program.cs`.
|
||||
> Implementation reference for Sprint 121 (`EXCITITOR-LNM-21-201`, `EXCITITOR-LNM-21-202`). Documents the REST endpoints implemented in `src/Excititor/StellaOps.Excititor.WebService/Endpoints/ObservationEndpoints.cs` and `LinksetEndpoints.cs`.
|
||||
|
||||
## /v1/vex/observations
|
||||
## Authentication & Headers
|
||||
|
||||
All endpoints require:
|
||||
- **Authorization**: Bearer token with `vex.read` scope
|
||||
- **X-Stella-Tenant**: Tenant identifier (required)
|
||||
|
||||
## /vex/observations
|
||||
|
||||
### List observations with filters
|
||||
|
||||
### List
|
||||
```
|
||||
GET /v1/vex/observations?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&providerId=ubuntu-csaf&status=affected&limit=2
|
||||
Headers:
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant: default
|
||||
Response 200 (application/json):
|
||||
GET /vex/observations?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&limit=50
|
||||
GET /vex/observations?providerId=ubuntu-csaf&limit=50
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `vulnerabilityId` + `productKey` (required together) - Filter by vulnerability and product
|
||||
- `providerId` - Filter by provider
|
||||
- `limit` (optional, default: 50, max: 100) - Number of results
|
||||
- `cursor` (optional) - Pagination cursor from previous response
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"observationId": "vex:obs:sha256:abc123...",
|
||||
"tenant": "default",
|
||||
"observationId": "vex:obs:sha256:...",
|
||||
"providerId": "ubuntu-csaf",
|
||||
"document": {
|
||||
"digest": "sha256:...",
|
||||
"uri": "https://example.com/csaf/1.json",
|
||||
"signature": null
|
||||
},
|
||||
"scope": {
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3"
|
||||
},
|
||||
"statements": [
|
||||
{
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"status": "affected",
|
||||
"justification": {
|
||||
"type": "component_not_present",
|
||||
"reason": "Not shipped in base profile"
|
||||
},
|
||||
"signals": { "severity": { "score": 7.5 } },
|
||||
"provenance": {
|
||||
"providerId": "ubuntu-csaf",
|
||||
"sourceId": "USN-9999-1",
|
||||
"fieldMasks": ["statements"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"linkset": {
|
||||
"aliases": ["USN-9999-1"],
|
||||
"purls": ["pkg:maven/org.demo/app"],
|
||||
"cpes": [],
|
||||
"references": [{"type": "advisory", "url": "https://..."}],
|
||||
"disagreements": []
|
||||
},
|
||||
"createdAt": "2025-11-18T12:34:56Z"
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"status": "affected",
|
||||
"createdAt": "2025-11-18T12:34:56Z",
|
||||
"lastObserved": "2025-11-18T12:34:56Z",
|
||||
"purls": ["pkg:maven/org.demo/app@1.2.3"]
|
||||
}
|
||||
],
|
||||
"nextCursor": "eyJ2dWxuZXJhYmlsaXR5SWQiOiJDVkUtMjAyNC0wMDAxIiwiY3JlYXRlZEF0IjoiMjAyNS0xMS0xOFQxMjozNDo1NloifQ=="
|
||||
"nextCursor": "MjAyNS0xMS0xOFQxMjozNDo1NlonfHZleDpvYnM6c2hhMjU2OmFiYzEyMy4uLg=="
|
||||
}
|
||||
```
|
||||
|
||||
### Get by key
|
||||
**Error Responses:**
|
||||
- `400 ERR_PARAMS` - At least one filter is required
|
||||
- `400 ERR_TENANT` - X-Stella-Tenant header is required
|
||||
- `403` - Missing required scope
|
||||
|
||||
### Get observation by ID
|
||||
|
||||
```
|
||||
GET /v1/vex/observations/CVE-2024-0001/pkg:maven/org.demo/app@1.2.3
|
||||
Headers: Authorization + X-Tenant
|
||||
Response 200: same projection shape as list items (single object).
|
||||
GET /vex/observations/{observationId}
|
||||
```
|
||||
|
||||
## /v1/vex/linksets
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"observationId": "vex:obs:sha256:abc123...",
|
||||
"tenant": "default",
|
||||
"providerId": "ubuntu-csaf",
|
||||
"streamId": "ubuntu-csaf-vex",
|
||||
"upstream": {
|
||||
"upstreamId": "USN-9999-1",
|
||||
"documentVersion": "2024.10.22",
|
||||
"fetchedAt": "2025-11-18T12:34:00Z",
|
||||
"receivedAt": "2025-11-18T12:34:05Z",
|
||||
"contentHash": "sha256:...",
|
||||
"signature": {
|
||||
"type": "cosign",
|
||||
"keyId": "ubuntu-vex-prod",
|
||||
"issuer": "https://token.actions.githubusercontent.com",
|
||||
"verifiedAt": "2025-11-18T12:34:10Z"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"format": "csaf",
|
||||
"specVersion": "2.0"
|
||||
},
|
||||
"statements": [
|
||||
{
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"status": "affected",
|
||||
"lastObserved": "2025-11-18T12:34:56Z",
|
||||
"locator": "#/statements/0",
|
||||
"justification": "component_not_present",
|
||||
"introducedVersion": null,
|
||||
"fixedVersion": "1.2.4"
|
||||
}
|
||||
],
|
||||
"linkset": {
|
||||
"aliases": ["USN-9999-1"],
|
||||
"purls": ["pkg:maven/org.demo/app@1.2.3"],
|
||||
"cpes": [],
|
||||
"references": [{"type": "advisory", "url": "https://ubuntu.com/security/notices/USN-9999-1"}]
|
||||
},
|
||||
"createdAt": "2025-11-18T12:34:56Z"
|
||||
}
|
||||
```
|
||||
GET /v1/vex/linksets?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&status=affected&limit=2
|
||||
Headers: Authorization + X-Tenant
|
||||
Response 200:
|
||||
|
||||
**Error Responses:**
|
||||
- `404 ERR_NOT_FOUND` - Observation not found
|
||||
|
||||
### Count observations
|
||||
|
||||
```
|
||||
GET /vex/observations/count
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"count": 12345
|
||||
}
|
||||
```
|
||||
|
||||
## /vex/linksets
|
||||
|
||||
### List linksets with filters
|
||||
|
||||
At least one filter is required: `vulnerabilityId`, `productKey`, `providerId`, or `hasConflicts=true`.
|
||||
|
||||
```
|
||||
GET /vex/linksets?vulnerabilityId=CVE-2024-0001&limit=50
|
||||
GET /vex/linksets?productKey=pkg:maven/org.demo/app@1.2.3&limit=50
|
||||
GET /vex/linksets?providerId=ubuntu-csaf&limit=50
|
||||
GET /vex/linksets?hasConflicts=true&limit=50
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `vulnerabilityId` - Filter by vulnerability ID
|
||||
- `productKey` - Filter by product key
|
||||
- `providerId` - Filter by provider
|
||||
- `hasConflicts` - Filter to linksets with disagreements (true/false)
|
||||
- `limit` (optional, default: 50, max: 100) - Number of results
|
||||
- `cursor` (optional) - Pagination cursor
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"linksetId": "CVE-2024-0001:pkg:maven/org.demo/app@1.2.3",
|
||||
"linksetId": "sha256:tenant:CVE-2024-0001:pkg:maven/org.demo/app@1.2.3",
|
||||
"tenant": "default",
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"providers": ["ubuntu-csaf", "suse-csaf"],
|
||||
"providerIds": ["ubuntu-csaf", "suse-csaf"],
|
||||
"statuses": ["affected", "fixed"],
|
||||
"aliases": ["USN-9999-1"],
|
||||
"purls": ["pkg:maven/org.demo/app"],
|
||||
"aliases": [],
|
||||
"purls": [],
|
||||
"cpes": [],
|
||||
"references": [{"type": "advisory", "url": "https://..."}],
|
||||
"disagreements": [{"providerId": "suse-csaf", "status": "fixed", "justification": null, "confidence": null}],
|
||||
"references": [],
|
||||
"disagreements": [
|
||||
{
|
||||
"providerId": "suse-csaf",
|
||||
"status": "fixed",
|
||||
"justification": null,
|
||||
"confidence": 0.85
|
||||
}
|
||||
],
|
||||
"observations": [
|
||||
{"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "severity": 7.5},
|
||||
{"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "severity": null}
|
||||
{"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "confidence": 0.9},
|
||||
{"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "confidence": 0.85}
|
||||
],
|
||||
"createdAt": "2025-11-18T12:34:56Z"
|
||||
}
|
||||
@@ -94,36 +171,152 @@ Response 200:
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Pagination: `limit` (default 200, max 500) + `cursor` (opaque base64 of `vulnerabilityId` + `createdAt`).
|
||||
- Filters: `vulnerabilityId`, `productKey`, `providerId`, `status`; multiple query values allowed.
|
||||
- Headers: `Excititor-Results-Count`, `Excititor-Results-Cursor` (observations) and `Excititor-Results-Total` / `Excititor-Results-Truncated` (chunks) already implemented.
|
||||
- Determinism: responses sorted by `vulnerabilityId`, then `productKey`; arrays sorted lexicographically.
|
||||
**Error Responses:**
|
||||
- `400 ERR_AGG_PARAMS` - At least one filter is required
|
||||
|
||||
## SDK generation
|
||||
- Source of truth for EXCITITOR-LNM-21-203 SDK samples (TypeScript/Go/Python) and OpenAPI snippets.
|
||||
- Suggested generation inputs:
|
||||
- Schema: this doc + `docs/modules/excititor/vex_observations.md` for field semantics.
|
||||
- Auth: bearer token + `X-Stella-Tenant` header (required).
|
||||
- Pagination: `cursor` (opaque) + `limit` (default 200, max 500).
|
||||
- Minimal client example (TypeScript, fetch):
|
||||
```ts
|
||||
const resp = await fetch(
|
||||
`${baseUrl}/v1/vex/observations?` + new URLSearchParams({
|
||||
vulnerabilityId: "CVE-2024-0001",
|
||||
productKey: "pkg:maven/org.demo/app@1.2.3",
|
||||
### Get linkset by ID
|
||||
|
||||
```
|
||||
GET /vex/linksets/{linksetId}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"linksetId": "sha256:...",
|
||||
"tenant": "default",
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"providerIds": ["ubuntu-csaf", "suse-csaf"],
|
||||
"statuses": ["affected", "fixed"],
|
||||
"confidence": "low",
|
||||
"hasConflicts": true,
|
||||
"disagreements": [
|
||||
{
|
||||
"providerId": "suse-csaf",
|
||||
"status": "fixed",
|
||||
"justification": null,
|
||||
"confidence": 0.85
|
||||
}
|
||||
],
|
||||
"observations": [
|
||||
{"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "confidence": 0.9},
|
||||
{"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "confidence": 0.85}
|
||||
],
|
||||
"createdAt": "2025-11-18T12:00:00Z",
|
||||
"updatedAt": "2025-11-18T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 ERR_AGG_PARAMS` - linksetId is required
|
||||
- `404 ERR_AGG_NOT_FOUND` - Linkset not found
|
||||
|
||||
### Lookup linkset by vulnerability and product
|
||||
|
||||
```
|
||||
GET /vex/linksets/lookup?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3
|
||||
```
|
||||
|
||||
**Response 200:** Same as Get linkset by ID
|
||||
|
||||
**Error Responses:**
|
||||
- `400 ERR_AGG_PARAMS` - vulnerabilityId and productKey are required
|
||||
- `404 ERR_AGG_NOT_FOUND` - No linkset found for the specified vulnerability and product
|
||||
|
||||
### Count linksets
|
||||
|
||||
```
|
||||
GET /vex/linksets/count
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"total": 5000,
|
||||
"withConflicts": 127
|
||||
}
|
||||
```
|
||||
|
||||
### List linksets with conflicts (shorthand)
|
||||
|
||||
```
|
||||
GET /vex/linksets/conflicts?limit=50
|
||||
```
|
||||
|
||||
**Response 200:** Same format as List linksets
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `ERR_PARAMS` | Missing or invalid query parameters (observations) |
|
||||
| `ERR_TENANT` | X-Stella-Tenant header is required |
|
||||
| `ERR_NOT_FOUND` | Observation not found |
|
||||
| `ERR_AGG_PARAMS` | Missing or invalid query parameters (linksets) |
|
||||
| `ERR_AGG_NOT_FOUND` | Linkset not found |
|
||||
|
||||
## Pagination
|
||||
|
||||
- Uses cursor-based pagination with base64-encoded `timestamp|id` cursors
|
||||
- Default limit: 50, Maximum limit: 100
|
||||
- Cursors are opaque; treat as strings and pass back unchanged
|
||||
|
||||
## Determinism
|
||||
|
||||
- Results are sorted by timestamp (descending), then by ID
|
||||
- Array fields are sorted lexicographically
|
||||
- Status enums are lowercase strings
|
||||
|
||||
## SDK Example (TypeScript)
|
||||
|
||||
```typescript
|
||||
const listObservations = async (
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
tenant: string,
|
||||
vulnerabilityId: string,
|
||||
productKey: string
|
||||
) => {
|
||||
const params = new URLSearchParams({
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
limit: "100"
|
||||
}),
|
||||
{
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/vex/observations?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Stella-Tenant": "default"
|
||||
"X-Stella-Tenant": tenant
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`${error.error.code}: ${error.error.message}`);
|
||||
}
|
||||
);
|
||||
const body = await resp.json();
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const getLinksetWithConflicts = async (
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
tenant: string
|
||||
) => {
|
||||
const response = await fetch(`${baseUrl}/vex/linksets/conflicts?limit=50`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-Stella-Tenant": tenant
|
||||
}
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
- Determinism requirements for SDKs:
|
||||
- Preserve server ordering; do not resort items client-side.
|
||||
- Treat `cursor` as opaque; echo it back for next page.
|
||||
- Keep enums case-sensitive as returned by API.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `vex_observations.md` - VEX Observation domain model and storage schema
|
||||
- `evidence-contract.md` - Evidence bundle format and attestation
|
||||
- `AGENTS.md` - Component development guidelines
|
||||
|
||||
@@ -120,9 +120,12 @@ All observation documents are immutable. New information creates a new observati
|
||||
|
||||
| API | Source fields | Notes |
|
||||
| --- | --- | --- |
|
||||
| `GET /vex/observations` | `tenant`, `vulnerabilityId`, `productKey`, `providerId` | List observations with filters. Implemented in `ObservationEndpoints.cs`. |
|
||||
| `GET /vex/observations/{observationId}` | `tenant`, `observationId` | Get single observation by ID with full detail. |
|
||||
| `GET /vex/observations/count` | `tenant` | Count all observations for tenant. |
|
||||
| `/v1/vex/observations/{vuln}/{product}` | `tenant`, `vulnerabilityId`, `productKey`, `scope`, `statements[]` | Response uses `VexObservationProjectionService` to render `statements`, `document`, and `signature` fields. |
|
||||
| `/vex/aoc/verify` | `document.digest`, `providerId`, `aoc` | Replays guard validation for recent digests; guard violations here align with `aoc.violations`. |
|
||||
| Evidence batch API (Graph) | `statements[]`, `scope`, `signals`, `anchors` | Format optimized for overlays; resuces `document` to digest/URI. |
|
||||
| Evidence batch API (Graph) | `statements[]`, `scope`, `signals`, `anchors` | Format optimized for overlays; reduces `document` to digest/URI. |
|
||||
|
||||
## Related work
|
||||
|
||||
|
||||
229
docs/modules/policy/design/deterministic-evaluator.md
Normal file
229
docs/modules/policy/design/deterministic-evaluator.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Deterministic Policy Evaluator Design
|
||||
|
||||
Status: Final
|
||||
Version: 1.0
|
||||
Owner: Policy Guild
|
||||
Last Updated: 2025-11-27
|
||||
|
||||
## Overview
|
||||
|
||||
The Policy Engine evaluator is designed for deterministic, reproducible execution. Given identical inputs, the evaluator produces byte-for-byte identical outputs regardless of host, timezone, or execution timing. This enables:
|
||||
|
||||
- Reproducible audit trails
|
||||
- Offline verification of policy decisions
|
||||
- Content-addressed caching of evaluation results
|
||||
- Bit-exact replay for debugging and compliance
|
||||
|
||||
## Contract and Guarantees
|
||||
|
||||
### Determinism Guarantees
|
||||
|
||||
1. **Input Determinism**: All inputs are content-addressed or explicitly provided via the evaluation context.
|
||||
2. **Output Determinism**: Given identical `PolicyEvaluationRequest`, the evaluator returns identical `PolicyEvaluationResult` objects.
|
||||
3. **Ordering Determinism**: Rule evaluation order is stable and deterministic.
|
||||
4. **Value Determinism**: All computed values use deterministic types (decimal vs float, immutable collections).
|
||||
|
||||
### Prohibited Operations
|
||||
|
||||
The following operations are **prohibited** during policy evaluation:
|
||||
|
||||
| Category | Prohibited | Rationale |
|
||||
|----------|-----------|-----------|
|
||||
| Wall-clock | `DateTime.Now`, `DateTime.UtcNow`, `DateTimeOffset.Now` | Non-deterministic |
|
||||
| Random | `Random`, `Guid.NewGuid()`, cryptographic RNG | Non-deterministic |
|
||||
| Network | `HttpClient`, socket operations, DNS lookups | External dependency |
|
||||
| Filesystem | File I/O during evaluation | External dependency |
|
||||
| Environment | `Environment.GetEnvironmentVariable()` | Host-dependent |
|
||||
|
||||
### Allowed Operations
|
||||
|
||||
| Category | Allowed | Usage |
|
||||
|----------|---------|-------|
|
||||
| Timestamps | `context.EvaluationTimestamp` | Injected evaluation time |
|
||||
| Identifiers | Deterministic ID generation from content | See `StableIdGenerator` |
|
||||
| Collections | `ImmutableArray<T>`, `ImmutableDictionary<K,V>` | Stable iteration order |
|
||||
| Arithmetic | `decimal` for numeric comparisons | Exact representation |
|
||||
|
||||
## Rule Ordering Semantics
|
||||
|
||||
### Evaluation Order
|
||||
|
||||
Rules are evaluated in the following deterministic order:
|
||||
|
||||
1. **Primary Sort**: `rule.Priority` (ascending - lower priority number evaluates first)
|
||||
2. **Secondary Sort**: Declaration order (index in the compiled IR document)
|
||||
|
||||
```csharp
|
||||
var orderedRules = document.Rules
|
||||
.Select((rule, index) => new { rule, index })
|
||||
.OrderBy(x => x.rule.Priority)
|
||||
.ThenBy(x => x.index)
|
||||
.ToImmutableArray();
|
||||
```
|
||||
|
||||
### First-Match Semantics
|
||||
|
||||
The evaluator uses first-match semantics:
|
||||
- Rules are evaluated in order until one matches
|
||||
- The first matching rule determines the base result
|
||||
- No further rules are evaluated after a match
|
||||
- If no rules match, a default result is returned
|
||||
|
||||
### Exception Application Order
|
||||
|
||||
When multiple exceptions could apply, specificity scoring determines the winner:
|
||||
|
||||
1. **Specificity Score**: Computed from scope constraints (rule names, severities, sources, tags)
|
||||
2. **Tie-breaker 1**: `CreatedAt` timestamp (later wins)
|
||||
3. **Tie-breaker 2**: `Id` lexicographic comparison (earlier wins)
|
||||
|
||||
This ensures deterministic exception selection even with identical specificity scores.
|
||||
|
||||
## Safe Value Types
|
||||
|
||||
### Numeric Types
|
||||
|
||||
| Use Case | Type | Rationale |
|
||||
|----------|------|-----------|
|
||||
| CVSS scores | `decimal` | Exact representation, no floating-point drift |
|
||||
| Priority | `int` | Integer ordering |
|
||||
| Severity comparisons | `decimal` via lookup table | Stable severity ordering |
|
||||
|
||||
The severity lookup table maps normalized severity strings to decimal values:
|
||||
|
||||
```csharp
|
||||
"critical" => 5m
|
||||
"high" => 4m
|
||||
"medium" => 3m
|
||||
"moderate" => 3m
|
||||
"low" => 2m
|
||||
"info" => 1m
|
||||
"none" => 0m
|
||||
"unknown" => -1m
|
||||
```
|
||||
|
||||
### String Comparisons
|
||||
|
||||
All string comparisons use `StringComparer.OrdinalIgnoreCase` for deterministic, culture-invariant comparison.
|
||||
|
||||
### Collection Types
|
||||
|
||||
| Collection | Usage |
|
||||
|------------|-------|
|
||||
| `ImmutableArray<T>` | Ordered sequences with stable iteration |
|
||||
| `ImmutableDictionary<K,V>` | Key-value stores |
|
||||
| `ImmutableHashSet<T>` | Membership tests |
|
||||
|
||||
## Timestamp Handling
|
||||
|
||||
### Context-Injected Timestamp
|
||||
|
||||
The evaluation timestamp is provided via the evaluation context, not read from the system clock:
|
||||
|
||||
```csharp
|
||||
public sealed record PolicyEvaluationContext(
|
||||
PolicyEvaluationSeverity Severity,
|
||||
PolicyEvaluationEnvironment Environment,
|
||||
PolicyEvaluationAdvisory Advisory,
|
||||
PolicyEvaluationVexEvidence Vex,
|
||||
PolicyEvaluationSbom Sbom,
|
||||
PolicyEvaluationExceptions Exceptions,
|
||||
DateTimeOffset EvaluationTimestamp); // Injected, not DateTime.UtcNow
|
||||
```
|
||||
|
||||
### Timestamp Format
|
||||
|
||||
All timestamps in outputs use ISO-8601 format with UTC timezone:
|
||||
|
||||
```
|
||||
2025-11-27T14:30:00.000Z
|
||||
```
|
||||
|
||||
## Expression Evaluation
|
||||
|
||||
### Boolean Expressions
|
||||
|
||||
Short-circuit evaluation is deterministic:
|
||||
- `AND`: Left-to-right, stops on first `false`
|
||||
- `OR`: Left-to-right, stops on first `true`
|
||||
|
||||
### Identifier Resolution
|
||||
|
||||
Identifiers resolve in deterministic order:
|
||||
1. Local scope (loop variables, predicates)
|
||||
2. Global context (`severity`, `env`, `vex`, `advisory`, `sbom`)
|
||||
3. Built-in constants (`true`, `false`)
|
||||
4. Null (unresolved)
|
||||
|
||||
### Member Access
|
||||
|
||||
Member access on scoped objects follows a fixed schema:
|
||||
- `severity.normalized`, `severity.score`
|
||||
- `advisory.source`, `advisory.<metadata-key>`
|
||||
- `vex.status`, `vex.justification`
|
||||
- `sbom.tags`, `sbom.components`
|
||||
|
||||
## Verification
|
||||
|
||||
### Content Hashing
|
||||
|
||||
Evaluation inputs and outputs can be content-addressed using SHA-256:
|
||||
|
||||
```
|
||||
Input Hash: SHA256(canonical_json(PolicyEvaluationRequest))
|
||||
Output Hash: SHA256(canonical_json(PolicyEvaluationResult))
|
||||
```
|
||||
|
||||
### Golden Test Vectors
|
||||
|
||||
Test vectors are provided in `docs/modules/policy/samples/deterministic-evaluator/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `test-vectors.json` | Input/output pairs with expected hashes |
|
||||
| `config-sample.yaml` | Sample evaluator configuration |
|
||||
|
||||
### Hash Recording
|
||||
|
||||
Each test vector records:
|
||||
- Input content hash
|
||||
- Expected output content hash
|
||||
- Human-readable input/output for inspection
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### PolicyEvaluator Class
|
||||
|
||||
Located at: `src/Policy/StellaOps.Policy.Engine/Evaluation/PolicyEvaluator.cs`
|
||||
|
||||
Key determinism features:
|
||||
- Uses `ImmutableArray` for ordered rule iteration
|
||||
- Exception selection uses deterministic tie-breaking
|
||||
- All collection operations preserve order
|
||||
|
||||
### PolicyExpressionEvaluator Class
|
||||
|
||||
Located at: `src/Policy/StellaOps.Policy.Engine/Evaluation/PolicyExpressionEvaluator.cs`
|
||||
|
||||
Key determinism features:
|
||||
- Uses decimal for numeric comparisons
|
||||
- Severity ordering via static lookup table
|
||||
- Immutable scope objects
|
||||
|
||||
## Compliance Checklist
|
||||
|
||||
Before shipping changes to the evaluator, verify:
|
||||
|
||||
- [ ] No `DateTime.Now` or `DateTime.UtcNow` usage in evaluation path
|
||||
- [ ] No `Random` or `Guid.NewGuid()` in evaluation path
|
||||
- [ ] No network or filesystem access in evaluation path
|
||||
- [ ] All collections use immutable types
|
||||
- [ ] Numeric comparisons use `decimal`
|
||||
- [ ] String comparisons use `StringComparer.OrdinalIgnoreCase`
|
||||
- [ ] Golden tests pass with recorded hashes
|
||||
|
||||
## References
|
||||
|
||||
- Prep document: `docs/modules/policy/prep/2025-11-20-policy-engine-20-002-prep.md`
|
||||
- Sprint task: POLICY-ENGINE-20-002 in `docs/implplan/SPRINT_124_policy_reasoning.md`
|
||||
- Implementation: `src/Policy/StellaOps.Policy.Engine/Evaluation/`
|
||||
@@ -0,0 +1,103 @@
|
||||
# Deterministic Evaluator Sample Configuration
|
||||
# This file demonstrates the configuration options for the policy evaluator
|
||||
# Version: 1.0
|
||||
|
||||
evaluator:
|
||||
# Determinism settings
|
||||
determinism:
|
||||
# Enforce strict determinism checks at runtime
|
||||
enforceStrict: true
|
||||
|
||||
# Log warnings for potential non-deterministic operations
|
||||
logWarnings: true
|
||||
|
||||
# Fail evaluation if non-deterministic operation detected
|
||||
failOnViolation: true
|
||||
|
||||
# Rule evaluation settings
|
||||
rules:
|
||||
# First-match semantics: stop on first matching rule
|
||||
firstMatchOnly: true
|
||||
|
||||
# Default status when no rules match
|
||||
defaultStatus: "affected"
|
||||
|
||||
# Enable priority-based ordering (lower priority evaluates first)
|
||||
priorityOrdering: true
|
||||
|
||||
# Exception handling settings
|
||||
exceptions:
|
||||
# Enable exception application after rule evaluation
|
||||
enabled: true
|
||||
|
||||
# Specificity weights for exception scope matching
|
||||
specificity:
|
||||
ruleNameBase: 1000
|
||||
ruleNamePerItem: 25
|
||||
severityBase: 500
|
||||
severityPerItem: 10
|
||||
sourceBase: 250
|
||||
sourcePerItem: 10
|
||||
tagBase: 100
|
||||
tagPerItem: 5
|
||||
|
||||
# Tie-breaker order: later CreatedAt wins, then lower Id wins
|
||||
tieBreaker:
|
||||
preferLaterCreatedAt: true
|
||||
preferLowerIdOnTie: true
|
||||
|
||||
# Value type settings
|
||||
values:
|
||||
# Use decimal for all numeric comparisons (no floating-point)
|
||||
useDecimalArithmetic: true
|
||||
|
||||
# Severity string-to-decimal mapping
|
||||
severityOrder:
|
||||
critical: 5
|
||||
high: 4
|
||||
medium: 3
|
||||
moderate: 3
|
||||
low: 2
|
||||
informational: 1
|
||||
info: 1
|
||||
none: 0
|
||||
unknown: -1
|
||||
|
||||
# Timestamp settings
|
||||
timestamps:
|
||||
# Format for all timestamp outputs
|
||||
format: "yyyy-MM-ddTHH:mm:ss.fffZ"
|
||||
|
||||
# Timezone for all timestamps (must be UTC for determinism)
|
||||
timezone: "UTC"
|
||||
|
||||
# Collection settings
|
||||
collections:
|
||||
# Use immutable collections for all internal state
|
||||
useImmutable: true
|
||||
|
||||
# String comparison mode for keys/lookups
|
||||
stringComparison: "OrdinalIgnoreCase"
|
||||
|
||||
# Content hashing settings for verification
|
||||
hashing:
|
||||
# Algorithm for content addressing
|
||||
algorithm: "SHA256"
|
||||
|
||||
# Include in output for audit trail
|
||||
includeInOutput: true
|
||||
|
||||
# Hash both input and output
|
||||
hashInputs: true
|
||||
hashOutputs: true
|
||||
|
||||
# Logging settings for determinism auditing
|
||||
logging:
|
||||
# Log rule evaluation order for debugging
|
||||
logRuleOrder: false
|
||||
|
||||
# Log exception selection for debugging
|
||||
logExceptionSelection: false
|
||||
|
||||
# Log final decision rationale
|
||||
logDecisionRationale: true
|
||||
@@ -0,0 +1,599 @@
|
||||
{
|
||||
"$schema": "https://stellaops.io/schemas/policy/test-vectors-v1.json",
|
||||
"version": "1.0",
|
||||
"description": "Deterministic evaluator test vectors with recorded input/output hashes",
|
||||
"generatedAt": "2025-11-27T00:00:00.000Z",
|
||||
"vectors": [
|
||||
{
|
||||
"id": "DEVAL-001",
|
||||
"name": "Critical severity blocks",
|
||||
"description": "Rule block_critical matches and returns blocked status",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "block_critical",
|
||||
"priority": 5,
|
||||
"when": "severity.normalized >= \"Critical\"",
|
||||
"then": "status := \"blocked\"",
|
||||
"because": "Critical severity must be remediated before deploy."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Critical",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "blocked",
|
||||
"severity": "Critical",
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"annotations": {},
|
||||
"warnings": [],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"outputSha256": "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-002",
|
||||
"name": "High severity with internet exposure escalates",
|
||||
"description": "Rule escalate_high_internet matches and escalates severity to Critical",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "escalate_high_internet",
|
||||
"priority": 10,
|
||||
"when": "severity.normalized == \"High\" and env.exposure == \"internet\"",
|
||||
"then": "escalate to severity_band(\"Critical\")",
|
||||
"because": "High severity on internet-exposed asset escalates to critical."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "High",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internet"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "affected",
|
||||
"severity": "Critical",
|
||||
"ruleName": "escalate_high_internet",
|
||||
"priority": 10,
|
||||
"annotations": {},
|
||||
"warnings": [],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-003",
|
||||
"name": "VEX override sets status and annotation",
|
||||
"description": "Rule require_vex_justification matches and sets status from VEX statement",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "require_vex_justification",
|
||||
"priority": 10,
|
||||
"when": "vex.any(status in [\"not_affected\",\"fixed\"]) and vex.justification in [\"component_not_present\",\"vulnerable_code_not_present\"]",
|
||||
"then": "status := vex.status; annotate winning_statement := vex.latest().statementId",
|
||||
"because": "Respect strong vendor VEX claims."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Medium",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": [
|
||||
{
|
||||
"status": "not_affected",
|
||||
"justification": "component_not_present",
|
||||
"statementId": "stmt-001",
|
||||
"timestamp": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "not_affected",
|
||||
"severity": "Medium",
|
||||
"ruleName": "require_vex_justification",
|
||||
"priority": 10,
|
||||
"annotations": {
|
||||
"winning_statement": "stmt-001"
|
||||
},
|
||||
"warnings": [],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-004",
|
||||
"name": "Exception suppresses critical finding",
|
||||
"description": "Exception with suppress effect overrides blocked status to suppressed",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "block_critical",
|
||||
"priority": 5,
|
||||
"when": "severity.normalized >= \"Critical\"",
|
||||
"then": "status := \"blocked\"",
|
||||
"because": "Critical severity must be remediated before deploy."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Critical",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {
|
||||
"suppress-critical": {
|
||||
"id": "suppress-critical",
|
||||
"name": "Critical Break Glass",
|
||||
"effect": "Suppress",
|
||||
"downgradeSeverity": null,
|
||||
"requiredControlId": null,
|
||||
"routingTemplate": "secops",
|
||||
"maxDurationDays": 7,
|
||||
"description": null
|
||||
}
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
"id": "exc-001",
|
||||
"effectId": "suppress-critical",
|
||||
"scope": {
|
||||
"ruleNames": ["block_critical"],
|
||||
"severities": [],
|
||||
"sources": [],
|
||||
"tags": []
|
||||
},
|
||||
"createdAt": "2025-10-01T00:00:00.000Z",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "suppressed",
|
||||
"severity": "Critical",
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"annotations": {
|
||||
"exception.id": "exc-001",
|
||||
"exception.effectId": "suppress-critical",
|
||||
"exception.effectType": "Suppress",
|
||||
"exception.effectName": "Critical Break Glass",
|
||||
"exception.routingTemplate": "secops",
|
||||
"exception.maxDurationDays": "7",
|
||||
"exception.status": "suppressed"
|
||||
},
|
||||
"warnings": [],
|
||||
"appliedException": {
|
||||
"exceptionId": "exc-001",
|
||||
"effectId": "suppress-critical",
|
||||
"effectType": "Suppress",
|
||||
"originalStatus": "blocked",
|
||||
"originalSeverity": "Critical",
|
||||
"appliedStatus": "suppressed",
|
||||
"appliedSeverity": "Critical",
|
||||
"metadata": {
|
||||
"routingTemplate": "secops",
|
||||
"maxDurationDays": "7",
|
||||
"effectName": "Critical Break Glass"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-005",
|
||||
"name": "More specific exception wins",
|
||||
"description": "Exception with higher specificity score wins over global exception",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "block_critical",
|
||||
"priority": 5,
|
||||
"when": "severity.normalized >= \"Critical\"",
|
||||
"then": "status := \"blocked\"",
|
||||
"because": "Critical severity must be remediated before deploy."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Critical",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {
|
||||
"suppress-critical-global": {
|
||||
"id": "suppress-critical-global",
|
||||
"name": "Global Critical Suppress",
|
||||
"effect": "Suppress"
|
||||
},
|
||||
"suppress-critical-rule": {
|
||||
"id": "suppress-critical-rule",
|
||||
"name": "Rule Critical Suppress",
|
||||
"effect": "Suppress"
|
||||
}
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
"id": "exc-global",
|
||||
"effectId": "suppress-critical-global",
|
||||
"scope": {
|
||||
"ruleNames": [],
|
||||
"severities": ["Critical"],
|
||||
"sources": [],
|
||||
"tags": []
|
||||
},
|
||||
"createdAt": "2025-09-01T00:00:00.000Z",
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"id": "exc-rule",
|
||||
"effectId": "suppress-critical-rule",
|
||||
"scope": {
|
||||
"ruleNames": ["block_critical"],
|
||||
"severities": ["Critical"],
|
||||
"sources": [],
|
||||
"tags": []
|
||||
},
|
||||
"createdAt": "2025-10-05T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"requestedBy": "alice"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "suppressed",
|
||||
"severity": "Critical",
|
||||
"ruleName": "block_critical",
|
||||
"priority": 5,
|
||||
"annotations": {
|
||||
"exception.id": "exc-rule",
|
||||
"exception.effectId": "suppress-critical-rule",
|
||||
"exception.effectType": "Suppress",
|
||||
"exception.effectName": "Rule Critical Suppress",
|
||||
"exception.status": "suppressed",
|
||||
"exception.meta.requestedBy": "alice"
|
||||
},
|
||||
"warnings": [],
|
||||
"appliedException": {
|
||||
"exceptionId": "exc-rule",
|
||||
"effectId": "suppress-critical-rule",
|
||||
"effectType": "Suppress",
|
||||
"originalStatus": "blocked",
|
||||
"originalSeverity": "Critical",
|
||||
"appliedStatus": "suppressed",
|
||||
"appliedSeverity": "Critical",
|
||||
"metadata": {
|
||||
"effectName": "Rule Critical Suppress",
|
||||
"requestedBy": "alice"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
},
|
||||
"notes": "exc-rule wins because rule name scope (1000 + 25) beats severity-only scope (500 + 10)"
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-006",
|
||||
"name": "No rule matches returns default",
|
||||
"description": "When no rules match, default result with affected status is returned",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Empty Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": []
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Low",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": false,
|
||||
"status": "affected",
|
||||
"severity": "Low",
|
||||
"ruleName": null,
|
||||
"priority": null,
|
||||
"annotations": {},
|
||||
"warnings": [],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-007",
|
||||
"name": "Warn rule emits warning and sets status",
|
||||
"description": "Rule with warn action emits warning message and sets warned status",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Baseline Production Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "alert_warn_eol_runtime",
|
||||
"priority": 1,
|
||||
"when": "severity.normalized <= \"Medium\" and sbom.has_tag(\"runtime:eol\")",
|
||||
"then": "warn message \"Runtime marked as EOL; upgrade recommended.\"",
|
||||
"because": "Deprecated runtime should be upgraded."
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Medium",
|
||||
"score": null
|
||||
},
|
||||
"environment": {
|
||||
"exposure": "internal"
|
||||
},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": ["runtime:eol"],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "warned",
|
||||
"severity": "Medium",
|
||||
"ruleName": "alert_warn_eol_runtime",
|
||||
"priority": 1,
|
||||
"annotations": {},
|
||||
"warnings": ["Runtime marked as EOL; upgrade recommended."],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DEVAL-008",
|
||||
"name": "Priority ordering ensures first-match semantics",
|
||||
"description": "Lower priority rule evaluates first and wins",
|
||||
"input": {
|
||||
"policy": {
|
||||
"name": "Priority Test Policy",
|
||||
"syntax": "stella-dsl@1",
|
||||
"rules": [
|
||||
{
|
||||
"name": "high_priority_rule",
|
||||
"priority": 1,
|
||||
"when": "true",
|
||||
"then": "status := \"high-priority-match\"",
|
||||
"because": "First priority wins"
|
||||
},
|
||||
{
|
||||
"name": "low_priority_rule",
|
||||
"priority": 10,
|
||||
"when": "true",
|
||||
"then": "status := \"low-priority-match\"",
|
||||
"because": "Never reached"
|
||||
}
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"severity": {
|
||||
"normalized": "Low",
|
||||
"score": null
|
||||
},
|
||||
"environment": {},
|
||||
"advisory": {
|
||||
"source": "GHSA",
|
||||
"metadata": {}
|
||||
},
|
||||
"vex": {
|
||||
"statements": []
|
||||
},
|
||||
"sbom": {
|
||||
"tags": [],
|
||||
"components": []
|
||||
},
|
||||
"exceptions": {
|
||||
"effects": {},
|
||||
"instances": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedOutput": {
|
||||
"matched": true,
|
||||
"status": "high-priority-match",
|
||||
"severity": "Low",
|
||||
"ruleName": "high_priority_rule",
|
||||
"priority": 1,
|
||||
"annotations": {},
|
||||
"warnings": [],
|
||||
"appliedException": null
|
||||
},
|
||||
"hashes": {
|
||||
"inputSha256": "placeholder-compute-at-runtime",
|
||||
"outputSha256": "placeholder-compute-at-runtime"
|
||||
},
|
||||
"notes": "Verifies first-match semantics with priority ordering"
|
||||
}
|
||||
],
|
||||
"deterministicProperties": {
|
||||
"ruleOrderingAlgorithm": "stable-sort by (priority ASC, declaration-index ASC)",
|
||||
"firstMatchSemantics": true,
|
||||
"exceptionSpecificityWeights": {
|
||||
"ruleNameBase": 1000,
|
||||
"ruleNamePerItem": 25,
|
||||
"severityBase": 500,
|
||||
"severityPerItem": 10,
|
||||
"sourceBase": 250,
|
||||
"sourcePerItem": 10,
|
||||
"tagBase": 100,
|
||||
"tagPerItem": 5
|
||||
},
|
||||
"exceptionTieBreaker": "later CreatedAt wins, then lower Id lexicographically wins",
|
||||
"numericType": "decimal",
|
||||
"stringComparison": "OrdinalIgnoreCase"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user