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:
37424
src/Web/StellaOps.Web/package-lock.json
generated
37424
src/Web/StellaOps.Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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">🔇</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">🔊</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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user