Implement InMemory Transport Layer for StellaOps Router

- Added InMemoryTransportOptions class for configuration settings including timeouts and latency.
- Developed InMemoryTransportServer class to handle connections, frame processing, and event management.
- Created ServiceCollectionExtensions for easy registration of InMemory transport services.
- Established project structure and dependencies for InMemory transport library.
- Implemented comprehensive unit tests for endpoint discovery, connection management, request/response flow, and streaming capabilities.
- Ensured proper handling of cancellation, heartbeat, and hello frames within the transport layer.
This commit is contained in:
StellaOps Bot
2025-12-05 01:00:10 +02:00
parent 8768c27f30
commit 175b750e29
111 changed files with 25407 additions and 19242 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "stellaops-web",
"version": "0.0.0",
{
"name": "stellaops-web",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -21,23 +21,23 @@
"node": ">=20.11.0",
"npm": ">=10.2.0"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.17",
"@angular/cli": "^17.3.17",
"private": true,
"dependencies": {
"@angular/animations": "^17.3.0",
"@angular/common": "^17.3.0",
"@angular/compiler": "^17.3.0",
"@angular/core": "^17.3.0",
"@angular/forms": "^17.3.0",
"@angular/platform-browser": "^17.3.0",
"@angular/platform-browser-dynamic": "^17.3.0",
"@angular/router": "^17.3.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.3.17",
"@angular/cli": "^17.3.17",
"@angular/compiler-cli": "^17.3.0",
"@axe-core/playwright": "4.8.4",
"@playwright/test": "^1.47.2",
@@ -45,16 +45,15 @@
"@storybook/addon-essentials": "8.1.0",
"@storybook/addon-interactions": "8.1.0",
"@storybook/angular": "8.1.0",
"@storybook/test": "8.1.0",
"@storybook/testing-library": "0.2.2",
"storybook": "8.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.2"
}
}
"@storybook/test": "^8.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"storybook": "^8.1.0",
"typescript": "~5.4.2"
}
}

View File

@@ -123,6 +123,14 @@ export interface PolicyRuleResult {
readonly passed: boolean;
readonly reason?: string;
readonly matchedItems?: readonly string[];
// Confidence metadata (UI-POLICY-13-007)
readonly unknownConfidence?: number | null;
readonly confidenceBand?: string | null;
readonly unknownAgeDays?: number | null;
readonly sourceTrust?: string | null;
readonly reachability?: string | null;
readonly quietedBy?: string | null;
readonly quiet?: boolean | null;
}
// AOC (Attestation of Compliance) chain entry

View File

@@ -968,8 +968,34 @@
{{ rule.passed ? '✓' : '✗' }}
</span>
<div class="rule-content">
<span class="rule-name">{{ rule.ruleName }}</span>
<code class="rule-id">{{ rule.ruleId }}</code>
<div class="rule-header">
<span class="rule-name">{{ rule.ruleName }}</span>
<code class="rule-id">{{ rule.ruleId }}</code>
</div>
<!-- Confidence and Quiet Metadata (UI-POLICY-13-007) -->
@if (rule.confidenceBand || rule.unknownConfidence !== null || rule.quiet) {
<div class="rule-metadata">
@if (rule.confidenceBand || rule.unknownConfidence !== null) {
<app-confidence-badge
[band]="rule.confidenceBand"
[confidence]="rule.unknownConfidence"
[ageDays]="rule.unknownAgeDays"
[showScore]="true"
[showAge]="rule.unknownAgeDays !== null"
/>
}
<app-quiet-provenance-indicator
[quiet]="rule.quiet ?? false"
[quietedBy]="rule.quietedBy"
[sourceTrust]="rule.sourceTrust"
[reachability]="rule.reachability"
[showDetails]="true"
[showWhenNotQuiet]="false"
/>
</div>
}
@if (rule.reason) {
<p class="rule-reason">{{ rule.reason }}</p>
}

View File

@@ -1117,20 +1117,36 @@ $color-text-muted: #6b7280;
min-width: 0;
}
.rule-header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
}
.rule-name {
display: block;
font-weight: 500;
color: #111827;
}
.rule-id {
display: block;
font-size: 0.75rem;
color: $color-text-muted;
background: rgba(0, 0, 0, 0.05);
padding: 0.125rem 0.25rem;
border-radius: 2px;
margin-top: 0.25rem;
}
// Confidence and quiet provenance metadata (UI-POLICY-13-007)
.rule-metadata {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
.rule-reason {

View File

@@ -31,6 +31,8 @@ import {
VexStatusSummary,
} from '../../core/api/evidence.models';
import { EvidenceApi, EVIDENCE_API } from '../../core/api/evidence.client';
import { ConfidenceBadgeComponent } from '../../shared/components/confidence-badge.component';
import { QuietProvenanceIndicatorComponent } from '../../shared/components/quiet-provenance-indicator.component';
type TabId = 'observations' | 'linkset' | 'vex' | 'policy' | 'aoc';
type ObservationView = 'side-by-side' | 'stacked';
@@ -38,7 +40,7 @@ type ObservationView = 'side-by-side' | 'stacked';
@Component({
selector: 'app-evidence-panel',
standalone: true,
imports: [CommonModule],
imports: [CommonModule, ConfidenceBadgeComponent, QuietProvenanceIndicatorComponent],
templateUrl: './evidence-panel.component.html',
styleUrls: ['./evidence-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -0,0 +1,250 @@
import { Component, Input, computed, input } from '@angular/core';
import { CommonModule } from '@angular/common';
/**
* Confidence band values matching backend PolicyUnknownConfidenceConfig.
*/
export type ConfidenceBand = 'high' | 'medium' | 'low' | 'unspecified';
/**
* Confidence badge component for displaying policy confidence metadata.
* Shows confidence band with color coding and optional age/score details.
*
* Confidence bands:
* - high (≥0.65): Fresh unknowns with recent telemetry
* - medium (≥0.35): Unknowns aging toward action required
* - low (≥0.0): Stale unknowns that must be triaged
*
* @see UI-POLICY-13-007
*/
@Component({
selector: 'app-confidence-badge',
standalone: true,
imports: [CommonModule],
template: `
<span
class="confidence-badge"
[class]="badgeClass()"
[attr.title]="tooltipText()"
[attr.aria-label]="ariaLabel()"
>
<span class="confidence-badge__band">{{ bandLabel() }}</span>
@if (showScore() && confidence() !== null) {
<span class="confidence-badge__score">{{ formatScore() }}</span>
}
@if (showAge() && ageDays() !== null) {
<span class="confidence-badge__age">{{ formatAge() }}</span>
}
</span>
`,
styles: [`
.confidence-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
cursor: help;
transition: opacity 0.15s;
&:hover {
opacity: 0.9;
}
}
.confidence-badge__band {
text-transform: uppercase;
letter-spacing: 0.025em;
}
.confidence-badge__score {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.confidence-badge__age {
font-size: 0.6875rem;
opacity: 0.85;
}
// Band-specific colors
.confidence-badge--high {
background: #dcfce7;
color: #15803d;
border: 1px solid #86efac;
}
.confidence-badge--medium {
background: #fef3c7;
color: #92400e;
border: 1px solid #fcd34d;
}
.confidence-badge--low {
background: #fee2e2;
color: #dc2626;
border: 1px solid #fca5a5;
}
.confidence-badge--unspecified {
background: #f3f4f6;
color: #6b7280;
border: 1px solid #d1d5db;
}
// Compact variant
.confidence-badge--compact {
padding: 0.125rem 0.375rem;
font-size: 0.6875rem;
.confidence-badge__score,
.confidence-badge__age {
display: none;
}
}
// Expanded variant with vertical layout
.confidence-badge--expanded {
flex-direction: column;
align-items: flex-start;
padding: 0.5rem 0.75rem;
.confidence-badge__band {
font-size: 0.8125rem;
}
.confidence-badge__score {
font-size: 1rem;
}
.confidence-badge__age {
font-size: 0.75rem;
margin-top: 0.125rem;
}
}
`],
})
export class ConfidenceBadgeComponent {
/**
* Confidence band: 'high', 'medium', 'low', or 'unspecified'.
*/
readonly band = input<ConfidenceBand | string | null>(null);
/**
* Numeric confidence score (0-1).
*/
readonly confidence = input<number | null>(null);
/**
* Age in days since unknown was first observed.
*/
readonly ageDays = input<number | null>(null);
/**
* Whether to show the numeric score.
*/
readonly showScore = input(false);
/**
* Whether to show the age in days.
*/
readonly showAge = input(false);
/**
* Display variant: 'default', 'compact', or 'expanded'.
*/
readonly variant = input<'default' | 'compact' | 'expanded'>('default');
protected readonly badgeClass = computed(() => {
const b = this.normalizedBand();
const v = this.variant();
const classes = [`confidence-badge--${b}`];
if (v !== 'default') {
classes.push(`confidence-badge--${v}`);
}
return classes.join(' ');
});
protected readonly normalizedBand = computed((): ConfidenceBand => {
const b = this.band();
if (b === 'high' || b === 'medium' || b === 'low') {
return b;
}
return 'unspecified';
});
protected readonly bandLabel = computed(() => {
const b = this.normalizedBand();
switch (b) {
case 'high':
return 'High';
case 'medium':
return 'Medium';
case 'low':
return 'Low';
default:
return 'Unknown';
}
});
protected readonly tooltipText = computed(() => {
const b = this.normalizedBand();
const conf = this.confidence();
const age = this.ageDays();
let text = '';
switch (b) {
case 'high':
text = 'High confidence: Fresh unknown with recent telemetry';
break;
case 'medium':
text = 'Medium confidence: Unknown aging toward action required';
break;
case 'low':
text = 'Low confidence: Stale unknown that must be triaged';
break;
default:
text = 'Confidence not specified';
}
if (conf !== null) {
text += ` (score: ${(conf * 100).toFixed(0)}%)`;
}
if (age !== null) {
text += ` | Age: ${this.formatAgeFull(age)}`;
}
return text;
});
protected readonly ariaLabel = computed(() => {
return `Confidence: ${this.bandLabel()}`;
});
protected formatScore(): string {
const conf = this.confidence();
if (conf === null) return '';
return `${(conf * 100).toFixed(0)}%`;
}
protected formatAge(): string {
const age = this.ageDays();
if (age === null) return '';
if (age < 1) return '<1d';
if (age < 7) return `${Math.round(age)}d`;
if (age < 30) return `${Math.round(age / 7)}w`;
return `${Math.round(age / 30)}mo`;
}
private formatAgeFull(days: number): string {
if (days < 1) return 'less than 1 day';
if (days === 1) return '1 day';
if (days < 7) return `${Math.round(days)} days`;
if (days < 14) return '1 week';
if (days < 30) return `${Math.round(days / 7)} weeks`;
if (days < 60) return '1 month';
return `${Math.round(days / 30)} months`;
}
}

View File

@@ -1,2 +1,4 @@
export { ExceptionBadgeComponent, ExceptionBadgeData } from './exception-badge.component';
export { ExceptionExplainComponent, ExceptionExplainData } from './exception-explain.component';
export { ConfidenceBadgeComponent, ConfidenceBand } from './confidence-badge.component';
export { QuietProvenanceIndicatorComponent } from './quiet-provenance-indicator.component';

View File

@@ -0,0 +1,309 @@
import { Component, computed, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
/**
* Quiet provenance indicator component for showing when a finding is suppressed.
* Displays the rule that quieted the finding with optional expand/collapse.
*
* "Quiet provenance" tracks:
* - quiet: boolean - Whether the finding is suppressed
* - quietedBy: string - Rule name that caused suppression
*
* This enables "explainably quiet by design" - suppressions with traceable justification.
*
* @see UI-POLICY-13-007
*/
@Component({
selector: 'app-quiet-provenance-indicator',
standalone: true,
imports: [CommonModule],
template: `
@if (quiet()) {
<div class="quiet-indicator" [class]="indicatorClass()">
<span class="quiet-indicator__icon" aria-hidden="true">&#x1F507;</span>
<div class="quiet-indicator__content">
<span class="quiet-indicator__label">Quieted</span>
@if (quietedBy()) {
<span class="quiet-indicator__by">
by <code class="quiet-indicator__rule">{{ quietedBy() }}</code>
</span>
}
</div>
@if (showDetails() && quietedBy()) {
<button
type="button"
class="quiet-indicator__toggle"
[attr.aria-expanded]="expanded()"
(click)="onToggle()"
>
{{ expanded() ? 'Hide' : 'Details' }}
</button>
}
</div>
@if (showDetails() && expanded()) {
<div class="quiet-indicator__details">
<dl>
<dt>Suppressed by Rule:</dt>
<dd><code>{{ quietedBy() }}</code></dd>
@if (sourceTrust()) {
<dt>Source Trust:</dt>
<dd>{{ sourceTrust() }}</dd>
}
@if (reachability()) {
<dt>Reachability:</dt>
<dd>
<span class="quiet-indicator__reachability" [class]="reachabilityClass()">
{{ reachabilityLabel() }}
</span>
</dd>
}
</dl>
</div>
}
} @else if (showWhenNotQuiet()) {
<div class="quiet-indicator quiet-indicator--active">
<span class="quiet-indicator__icon" aria-hidden="true">&#x1F50A;</span>
<span class="quiet-indicator__label">Active</span>
</div>
}
`,
styles: [`
.quiet-indicator {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.625rem;
border-radius: 6px;
font-size: 0.8125rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
}
.quiet-indicator__icon {
font-size: 1rem;
}
.quiet-indicator__content {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.25rem;
}
.quiet-indicator__label {
font-weight: 600;
color: #374151;
}
.quiet-indicator__by {
font-size: 0.75rem;
color: #6b7280;
}
.quiet-indicator__rule {
background: #e5e7eb;
padding: 0.125rem 0.25rem;
border-radius: 3px;
font-size: 0.6875rem;
}
.quiet-indicator__toggle {
margin-left: auto;
padding: 0.125rem 0.375rem;
border: 1px solid #d1d5db;
border-radius: 3px;
background: #fff;
font-size: 0.6875rem;
color: #3b82f6;
cursor: pointer;
&:hover {
background: #eff6ff;
border-color: #3b82f6;
}
&:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
}
.quiet-indicator__details {
margin-top: 0.5rem;
padding: 0.75rem;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
dl {
margin: 0;
font-size: 0.8125rem;
}
dt {
color: #6b7280;
margin-top: 0.5rem;
&:first-child {
margin-top: 0;
}
}
dd {
margin: 0.25rem 0 0;
color: #111827;
code {
background: #e5e7eb;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.75rem;
}
}
}
.quiet-indicator__reachability {
display: inline-block;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 500;
}
// Reachability-specific colors
.quiet-indicator__reachability--unreachable {
background: #dcfce7;
color: #15803d;
}
.quiet-indicator__reachability--indirect {
background: #dbeafe;
color: #2563eb;
}
.quiet-indicator__reachability--direct {
background: #fef9c3;
color: #a16207;
}
.quiet-indicator__reachability--runtime {
background: #fee2e2;
color: #dc2626;
}
.quiet-indicator__reachability--entrypoint {
background: #fee2e2;
color: #dc2626;
}
.quiet-indicator__reachability--unknown {
background: #f3f4f6;
color: #6b7280;
}
// Active (not quieted) variant
.quiet-indicator--active {
background: #dbeafe;
border-color: #93c5fd;
.quiet-indicator__label {
color: #2563eb;
}
}
// Compact variant
.quiet-indicator--compact {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
.quiet-indicator__icon {
font-size: 0.875rem;
}
.quiet-indicator__by {
display: none;
}
}
`],
})
export class QuietProvenanceIndicatorComponent {
/**
* Whether the finding is quieted/suppressed.
*/
readonly quiet = input(false);
/**
* Name of the rule that quieted the finding.
*/
readonly quietedBy = input<string | null>(null);
/**
* Source trust identifier.
*/
readonly sourceTrust = input<string | null>(null);
/**
* Reachability bucket.
*/
readonly reachability = input<string | null>(null);
/**
* Whether to show the expand/collapse details toggle.
*/
readonly showDetails = input(false);
/**
* Whether to show indicator when finding is NOT quieted.
*/
readonly showWhenNotQuiet = input(false);
/**
* Display variant: 'default' or 'compact'.
*/
readonly variant = input<'default' | 'compact'>('default');
/**
* Whether details are expanded.
*/
readonly expanded = input(false);
/**
* Emitted when expand/collapse is toggled.
*/
readonly expandedChange = output<boolean>();
protected readonly indicatorClass = computed(() => {
const v = this.variant();
return v === 'compact' ? 'quiet-indicator--compact' : '';
});
protected readonly reachabilityClass = computed(() => {
const r = this.reachability();
if (!r) return 'quiet-indicator__reachability--unknown';
return `quiet-indicator__reachability--${r.toLowerCase()}`;
});
protected readonly reachabilityLabel = computed(() => {
const r = this.reachability();
if (!r) return 'Unknown';
switch (r.toLowerCase()) {
case 'unreachable':
return 'Unreachable';
case 'indirect':
return 'Indirect';
case 'direct':
return 'Direct';
case 'runtime':
return 'Runtime';
case 'entrypoint':
return 'Entry Point';
default:
return r;
}
});
protected onToggle(): void {
this.expandedChange.emit(!this.expanded());
}
}