search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -0,0 +1,981 @@
// -----------------------------------------------------------------------------
// unified-search-cross-domain.e2e.spec.ts
// E2E tests for cross-domain natural language queries (410 cases),
// domain filtering, keyboard shortcuts, ambient context, edge cases,
// and comprehensive accessibility.
// -----------------------------------------------------------------------------
import { expect, test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import {
buildResponse,
doctorCard,
docsCard,
emptyResponse,
findingCard,
mockSearchApi,
mockSearchApiDynamic,
policyCard,
secretCard,
vexCard,
setupAuthenticatedSession,
setupBasicMocks,
typeInSearch,
waitForEntityCards,
waitForResults,
waitForShell,
} from './unified-search-fixtures';
// ---------------------------------------------------------------------------
// Cross-domain fixtures (multi-domain responses)
// ---------------------------------------------------------------------------
const troubleshootDbResponse = buildResponse('cannot connect to database', [
doctorCard({
checkId: 'check.postgres.connectivity',
title: 'Postgres Connectivity',
snippet: 'Validates TCP/TLS connectivity to the primary Postgres cluster.',
score: 0.95,
}),
doctorCard({
checkId: 'check.postgres.pool',
title: 'Postgres Connection Pool',
snippet: 'Connection pool utilization and health thresholds.',
score: 0.80,
}),
docsCard({
docPath: 'db/local-postgres.md',
title: 'Local Postgres Setup',
snippet: 'Guide for configuring local PostgreSQL for development.',
score: 0.60,
}),
], {
summary: 'Database connectivity issue. Run `stella doctor run check.postgres.*` to diagnose. See Postgres setup docs.',
template: 'troubleshoot',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['knowledge'],
});
const troubleshootAuthResponse = buildResponse('authentication failed', [
doctorCard({
checkId: 'check.auth.config',
title: 'Auth Configuration',
snippet: 'Validates OIDC provider configuration and token service health.',
score: 0.92,
}),
doctorCard({
checkId: 'check.auth.oidc',
title: 'OIDC Provider Connectivity',
snippet: 'Checks connectivity to the configured OIDC identity provider.',
score: 0.85,
}),
docsCard({
docPath: 'modules/authority/architecture.md',
title: 'Authority Architecture',
snippet: 'OAuth 2.1 / OIDC authority module with DPoP authentication.',
score: 0.55,
}),
]);
const troubleshootSigningResponse = buildResponse('attestation signing failed HSM timeout', [
doctorCard({
checkId: 'check.crypto.hsm',
title: 'HSM PKCS#11 Availability',
snippet: 'HSM module connectivity check — timeout detected.',
score: 0.94,
}),
doctorCard({
checkId: 'check.attestation.keymaterial',
title: 'Signing Key Expiration',
snippet: 'Checks key material health and expiration status.',
score: 0.82,
}),
docsCard({
docPath: 'modules/signer/architecture.md',
title: 'Signer Architecture',
snippet: 'Key management, HSM integration, and signing ceremony documentation.',
score: 0.60,
}),
]);
const troubleshootTimestampResponse = buildResponse('TSA endpoint not responding', [
doctorCard({
checkId: 'check.timestamp.tsa.availability',
title: 'TSA Availability',
snippet: 'RFC-3161 TSA endpoint health probe — connection timeout.',
score: 0.96,
}),
doctorCard({
checkId: 'check.timestamp.tsa.failover-ready',
title: 'TSA Failover Ready',
snippet: 'Checks if failover TSA endpoint is configured and responsive.',
score: 0.78,
}),
]);
const troubleshootIntegrationResponse = buildResponse('OCI registry credentials expired', [
doctorCard({
checkId: 'check.integration.oci.credentials',
title: 'OCI Registry Credentials',
snippet: 'Registry credentials validation — token expired.',
score: 0.93,
}),
doctorCard({
checkId: 'check.integration.oci.registry',
title: 'OCI Registry',
snippet: 'Non-destructive HEAD probe to verify registry availability.',
score: 0.80,
}),
]);
const howToScanResponse = buildResponse('how to scan a container', [
docsCard({
docPath: 'quickstart.md',
title: 'Quick Start Guide',
snippet: 'Step-by-step: scanning your first container image with Stella Ops.',
score: 0.90,
}),
docsCard({
docPath: 'modules/scanner/architecture.md',
title: 'Scanner Architecture',
snippet: 'Container image scanning pipeline, SBOM generation, and vulnerability detection.',
score: 0.72,
}),
]);
const howToDoctorResponse = buildResponse('how to run doctor checks', [
docsCard({
docPath: 'modules/doctor/architecture.md',
title: 'Doctor Architecture',
snippet: 'Health check plugin system with 134+ checks across 18 plugins.',
score: 0.88,
}),
doctorCard({
checkId: 'check.postgres.connectivity',
title: 'Example: Postgres Connectivity',
snippet: 'Run: stella doctor run check.postgres.connectivity',
score: 0.65,
}),
]);
const navigationResponse = buildResponse('open settings', [
docsCard({
docPath: 'setup/overview',
title: 'Setup & Configuration',
snippet: 'Navigate to /setup to configure topology, agents, and integrations.',
score: 0.85,
}),
]);
const cliResponse = buildResponse('stella release create', [
docsCard({
docPath: 'API_CLI_REFERENCE.md',
title: 'CLI Reference: stella release create',
snippet: 'Creates a new release version. Usage: stella release create --name v1.2.0 --target production',
score: 0.92,
}),
]);
const conceptResponse = buildResponse('what is a VEX statement', [
docsCard({
docPath: 'VEX_CONSENSUS_GUIDE.md',
title: 'VEX Consensus Guide',
snippet: 'A VEX statement declares whether a product is affected by a specific vulnerability.',
score: 0.95,
}),
docsCard({
docPath: 'GLOSSARY.md',
title: 'Glossary: VEX',
snippet: 'Vulnerability Exploitability eXchange — a machine-readable format for communicating vulnerability status.',
score: 0.72,
}),
]);
const multiDomainCveResponse = buildResponse('CVE-2024-21626 runc VEX', [
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626: Container Escape via runc',
snippet: 'CRITICAL container escape vulnerability in runc 1.1.10.',
severity: 'critical',
score: 0.97,
}),
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'VEX: CVE-2024-21626 — Not Affected',
snippet: 'Vendor VEX: component_not_present in production image.',
score: 0.85,
}),
docsCard({
docPath: 'modules/scanner/architecture.md',
title: 'Container Scanning Documentation',
snippet: 'Guide for scanning container images and handling runc-related findings.',
score: 0.60,
}),
], {
summary: 'CVE-2024-21626: CRITICAL finding with VEX "not_affected" from vendor. 3 results across findings, VEX, and docs.',
template: 'cve_summary',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['findings', 'vex', 'knowledge'],
});
const policyFindingCrossResponse = buildResponse('release blocked by reachable CVE', [
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626 (REACHABLE)',
snippet: 'Container escape — reachability confirmed by static + runtime analysis.',
severity: 'critical',
score: 0.95,
}),
policyCard({
ruleId: 'reachable-cve-gate-block',
title: 'ReachableCveGate: BLOCKED',
snippet: 'Release promotion blocked. Reachable critical CVE without VEX justification.',
score: 0.90,
}),
vexCard({
cveId: 'CVE-2024-21626',
status: 'under_investigation',
title: 'VEX: Under Investigation',
snippet: 'No VEX justification available yet. Under investigation by vendor.',
score: 0.70,
}),
], {
summary: 'Release BLOCKED: CVE-2024-21626 is reachable, VEX under investigation. ReachableCveGate triggered.',
template: 'mixed_overview',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['findings', 'policy', 'vex'],
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Cross-Domain Queries', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 7.1 Troubleshooting Queries
// -------------------------------------------------------------------------
test.describe('Troubleshooting Queries', () => {
const dbTroubleshoot = [
'cannot connect to database',
'database connection failing',
'connection pool exhausted',
'postgres connectivity lost',
'migration failed',
'slow query performance',
];
for (const query of dbTroubleshoot) {
test(`DB: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootDbResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const authTroubleshoot = [
'authentication failed',
'token expired',
'error 403 forbidden',
'OIDC provider connectivity',
];
for (const query of authTroubleshoot) {
test(`Auth: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootAuthResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const signingTroubleshoot = [
'signing failed',
'attestation signing failed HSM timeout',
'HSM not available',
'cosign key material',
];
for (const query of signingTroubleshoot) {
test(`Signing: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootSigningResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const timestampTroubleshoot = [
'TSA endpoint not responding',
'TSA response time degraded',
'TSA certificate about to expire',
'OCSP responder unreachable',
'CRL distribution endpoint down',
'timestamp token approaching expiry',
'system clock not synced NTP',
'Rekor time correlation drift',
];
for (const query of timestampTroubleshoot) {
test(`TSA: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootTimestampResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const integrationTroubleshoot = [
'OCI registry credentials expired',
'registry push denied insufficient permissions',
'S3 bucket access denied',
'Slack API rate limited',
'Git provider SSH key rejected',
'LDAP bind failed wrong credentials',
'secrets manager Vault sealed',
];
for (const query of integrationTroubleshoot) {
test(`Integration: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootIntegrationResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const agentTroubleshoot = [
'agent not responding',
'agent version mismatch in cluster',
'agent certificate expired',
'agent resource utilization critical',
'stale agent not reporting',
'agent task failure rate above threshold',
'cluster health degraded',
'quorum lost',
];
for (const query of agentTroubleshoot) {
test(`Agent: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootDbResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const complianceTroubleshoot = [
'compliance evidence integrity violation',
'provenance chain validation failed',
'audit readiness check failed',
'evidence generation rate dropped',
'export readiness not met',
'evidence tampering detected',
];
for (const query of complianceTroubleshoot) {
test(`Compliance: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootDbResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const cryptoTroubleshoot = [
'eIDAS compliance check failing',
'FIPS module not loaded',
'HSM PKCS#11 module unavailable',
'GOST crypto provider not found',
'SM2/SM3/SM4 provider missing',
];
for (const query of cryptoTroubleshoot) {
test(`Crypto: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootSigningResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 7.2 How-To & Workflow Queries
// -------------------------------------------------------------------------
test.describe('How-To Queries', () => {
const howToQueries = [
'how to scan a container',
'how to create a release',
'how to promote to production',
'how to triage a finding',
'how to suppress a vulnerability',
'how to generate SBOM',
'how to write a VEX statement',
'how to configure notifications',
'how to set up policy gates',
'how to export evidence',
'how to verify attestation',
'how to configure air gap mode',
'how to rotate signing keys',
'how to run doctor checks',
'how to create policy exception',
'how to investigate reachability',
'how to configure Prometheus',
'how to set up email alerts',
'how to configure escalation',
'how to deploy offline',
];
for (const query of howToQueries) {
test(`"${query}" returns documentation cards`, async ({ page }) => {
const fixture = query.includes('doctor') ? howToDoctorResponse : howToScanResponse;
await mockSearchApi(page, fixture);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 7.3 Navigation & Feature Discovery
// -------------------------------------------------------------------------
test.describe('Navigation Queries', () => {
const navQueries = [
'open settings',
'go to findings',
'show dashboard',
'open security view',
'go to policy gates',
'open VEX hub',
'show release history',
'open agent fleet',
'go to evidence center',
'open timeline',
'go to triage inbox',
'open approval queue',
'show integrations',
'open policy studio',
'open doctor diagnostics',
'where is the audit log',
'find the compliance dashboard',
'open mission control',
];
for (const query of navQueries) {
test(`nav: "${query}"`, async ({ page }) => {
await mockSearchApi(page, navigationResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 7.4 CLI Command Searches
// -------------------------------------------------------------------------
test.describe('CLI Command Searches', () => {
const cliQueries = [
'stella release create',
'stella release promote',
'stella scan graph',
'stella policy validate-yaml',
'stella policy simulate',
'stella doctor run',
'stella vex generate',
'stella evidence export',
'stella attest sign',
'stella verify',
'stella db migrate',
'stella airgap prepare',
'stella agent status',
'stella crypto keygen',
'stella sbom generate',
'stella notify test',
'stella feeds sync',
'stella delta compare',
'stella reachability check',
'stella auth login',
];
for (const query of cliQueries) {
test(`CLI: "${query}"`, async ({ page }) => {
await mockSearchApi(page, cliResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 7.5 Concept & Explanation Queries
// -------------------------------------------------------------------------
test.describe('Concept Queries', () => {
const conceptQueries = [
'what is a VEX statement',
'explain SBOM',
'what is reachability analysis',
'explain attestation',
'what is a policy gate',
'explain risk budget',
'what is severity fusion',
'what is deterministic replay',
'explain provenance',
'what is sealed mode',
'what is content addressable storage',
'explain smart diff',
'what is a linkset',
'what is the findings ledger',
'explain reciprocal rank fusion',
'what is a verdict',
'explain storm breaker',
'what is a dead letter queue',
'explain circuit breaker pattern',
'what is DPoP authentication',
];
for (const query of conceptQueries) {
test(`concept: "${query}"`, async ({ page }) => {
await mockSearchApi(page, conceptResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 7.8 Multi-Domain Operational Queries
// -------------------------------------------------------------------------
test.describe('Multi-Domain Operational Queries', () => {
const multiDomainQueries = [
'CVE-2024-21626 runc escape reachability VEX',
'log4j affected not_affected VEX',
'how to suppress CVE-2023-44487',
'VEX not_affected but policy still blocks',
'reachability shows vulnerable code not in execute path',
];
for (const query of multiDomainQueries) {
test(`multi-domain: "${query}" renders cards from multiple domains`, async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
const cards = await waitForEntityCards(page);
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(2);
});
}
test('multi-domain synthesis covers all domains', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626 runc VEX');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('CVE-2024-21626');
});
const policyFindingQueries = [
'release blocked by reachable CVE and no VEX',
'policy violation critical CVE-2024-3094',
'promote release with blocked findings',
'VEX consensus disagreement blocking release',
];
for (const query of policyFindingQueries) {
test(`policy+finding: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyFindingCrossResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const operationalQueries = [
'SBOM incomplete missing Go dependencies',
'binary analysis found crypto weakness',
'environment drift detected after deployment',
'policy determinism check failed in sealed mode',
'feed mirror stale advisory data 7 days old',
'CI integration broken OIDC token expired',
'scheduler missed nightly scan job',
'agent fleet partial quorum during upgrade',
'Prometheus not collecting scanner metrics',
'trust anchor expired blocking attestation',
'evidence staleness exceeding policy TTL',
'findings backlog prioritization by EPSS',
];
for (const query of operationalQueries) {
test(`operational: "${query}"`, async ({ page }) => {
await mockSearchApi(page, troubleshootDbResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
});
// ---------------------------------------------------------------------------
// Domain Filtering Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Domain Filtering', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
test('domain filter chips render for multi-domain results', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626 VEX');
await waitForResults(page);
await waitForEntityCards(page);
const resultsContainer = page.locator('.search__results');
const filters = resultsContainer.locator('[data-role="domain-filter"], .search__filters .search__filter, .search__filter, .chip');
const count = await filters.count();
expect(count).toBeGreaterThanOrEqual(1);
});
test('clicking domain filter narrows results', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626 VEX');
await waitForResults(page);
const filters = page.locator('.search__filter, [data-role="domain-filter"]');
if ((await filters.count()) > 0) {
await filters.first().click();
// Results should still be visible (filtered view)
await expect(page.locator('.search__results')).toBeVisible();
}
});
});
// ---------------------------------------------------------------------------
// Keyboard Navigation Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Keyboard Navigation', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
test('ArrowDown moves through entity cards', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
const input = await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
await waitForEntityCards(page, 3);
// Navigate down through cards
await input.press('ArrowDown');
await input.press('ArrowDown');
await input.press('ArrowDown');
// Cards should still be visible
await expect(page.locator('app-entity-card').first()).toBeVisible();
});
test('Escape closes results, second Escape blurs input', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
// First Escape closes results
await page.keyboard.press('Escape');
await expect(page.locator('.search__results')).toBeHidden({ timeout: 5_000 });
});
test('Enter on selected card triggers primary action', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
const input = await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
await waitForEntityCards(page);
await input.press('ArrowDown');
const navigationPromise = page.waitForURL(/\/security\/(triage|advisories-vex)/, { timeout: 10_000 });
await page.keyboard.press('Enter');
await navigationPromise;
expect(page.url()).toMatch(/\/security\/(triage|advisories-vex)/);
});
});
// ---------------------------------------------------------------------------
// Empty State & Edge Cases
// ---------------------------------------------------------------------------
test.describe('Unified Search — Edge Cases', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
test('empty query does not trigger search', async ({ page }) => {
await mockSearchApi(page, emptyResponse(''));
await waitForShell(page);
const input = page.locator('app-global-search input[type="text"]');
await input.click();
await input.fill('');
// Short wait — results should NOT appear for empty query
await page.waitForTimeout(500);
await expect(page.locator('.search__results')).toBeHidden();
});
test('single character does not trigger search (min 2 chars)', async ({ page }) => {
await mockSearchApi(page, emptyResponse('a'));
await waitForShell(page);
const input = page.locator('app-global-search input[type="text"]');
await input.click();
await input.fill('a');
await page.waitForTimeout(500);
await expect(page.locator('.search__results .search__cards')).toBeHidden();
});
test('no results shows empty state message', async ({ page }) => {
await mockSearchApi(page, emptyResponse('xyznonexistent999'));
await waitForShell(page);
await typeInSearch(page, 'xyznonexistent999');
await waitForResults(page);
const entityCards = page.locator('app-entity-card');
await expect(entityCards).toHaveCount(0, { timeout: 5_000 });
const noResults = page.locator('.search__results').getByText(/no results/i);
await expect(noResults).toBeVisible({ timeout: 5_000 });
});
test('very long query string does not crash UI', async ({ page }) => {
const longQuery = 'CVE-2024-21626 '.repeat(50).trim();
await mockSearchApi(page, emptyResponse(longQuery));
await waitForShell(page);
await typeInSearch(page, longQuery);
// Should not throw; results container should appear
await page.waitForTimeout(1000);
await expect(page.locator('.search__results')).toBeVisible({ timeout: 10_000 });
});
test('special characters in query are handled safely', async ({ page }) => {
const specialQuery = '<script>alert(1)</script> CVE-2024-21626 "test" & | ; $()';
await mockSearchApi(page, emptyResponse(specialQuery));
await waitForShell(page);
await typeInSearch(page, specialQuery);
// Should not crash or inject scripts
await page.waitForTimeout(500);
// Verify page is still functional
await expect(page.locator('aside.sidebar')).toBeVisible();
});
test('rapid typing triggers debounce (only one API call)', async ({ page }) => {
let apiCallCount = 0;
await page.route('**/search/query**', (route) => {
apiCallCount++;
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(emptyResponse('test')),
});
});
await waitForShell(page);
const input = page.locator('app-global-search input[type="text"]');
await input.click();
// Type rapidly, character by character
for (const char of 'CVE-2024-21626') {
await input.type(char, { delay: 20 });
}
// Wait for debounce to settle
await page.waitForTimeout(500);
// Should have made fewer calls than characters typed (debounce at 200ms)
expect(apiCallCount).toBeLessThan(14);
});
test('API error gracefully handled', async ({ page }) => {
await page.route('**/search/query**', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
}),
);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
// Wait and verify the page doesn't crash
await page.waitForTimeout(1000);
await expect(page.locator('aside.sidebar')).toBeVisible();
});
test('network timeout gracefully handled', async ({ page }) => {
await page.route('**/search/query**', (route) => route.abort('timedout'));
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await page.waitForTimeout(1000);
await expect(page.locator('aside.sidebar')).toBeVisible();
});
});
// ---------------------------------------------------------------------------
// Accessibility Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Accessibility', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
test('search input has proper ARIA attributes', async ({ page }) => {
await waitForShell(page);
const input = page.locator('app-global-search input[type="text"]');
await expect(input).toHaveAttribute('aria-label', /search/i);
await expect(input).toHaveAttribute('aria-autocomplete', 'list');
});
test('entity cards are present in the results list', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
await waitForEntityCards(page);
const cards = page.locator('.search__cards app-entity-card');
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(1);
});
test('synthesis panel has role="region" and aria-label', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel [role="region"]');
if ((await synthesis.count()) > 0) {
await expect(synthesis).toHaveAttribute('aria-label', /synthesis|summary/i);
}
});
test('results listbox has proper ARIA', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
const listbox = page.locator('#search-results, [role="listbox"]');
await expect(listbox).toBeVisible();
});
test('findings results pass axe-core WCAG 2.0 AA', async ({ page }) => {
await mockSearchApi(page, multiDomainCveResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
await waitForEntityCards(page, 3);
const a11y = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.include('.search')
.analyze();
const serious = a11y.violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious',
);
if (serious.length > 0) {
console.log(
'[a11y] violations:',
JSON.stringify(serious.map((v) => ({ id: v.id, impact: v.impact, nodes: v.nodes.length })), null, 2),
);
}
expect(serious, `${serious.length} critical/serious a11y violations`).toHaveLength(0);
});
test('doctor results pass axe-core', async ({ page }) => {
await mockSearchApi(page, troubleshootDbResponse);
await waitForShell(page);
await typeInSearch(page, 'database connection');
await waitForResults(page);
await waitForEntityCards(page);
const a11y = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.include('.search')
.analyze();
const serious = a11y.violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious',
);
expect(serious).toHaveLength(0);
});
test('empty state passes axe-core', async ({ page }) => {
await mockSearchApi(page, emptyResponse('nonexistent'));
await waitForShell(page);
await typeInSearch(page, 'nonexistent');
await waitForResults(page);
const a11y = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.include('.search')
.analyze();
const serious = a11y.violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious',
);
expect(serious).toHaveLength(0);
});
});

View File

@@ -0,0 +1,577 @@
// -----------------------------------------------------------------------------
// unified-search-doctor.e2e.spec.ts
// E2E tests for Doctor Check domain queries (180 cases from test-cases.md).
// Verifies: doctor entity cards, check code rendering, run actions,
// synthesis templates, severity badges, and domain filtering.
// -----------------------------------------------------------------------------
import { expect, test } from '@playwright/test';
import {
buildResponse,
doctorCard,
docsCard,
mockSearchApi,
mockSearchApiDynamic,
setupAuthenticatedSession,
setupBasicMocks,
typeInSearch,
waitForEntityCards,
waitForResults,
waitForShell,
} from './unified-search-fixtures';
// ---------------------------------------------------------------------------
// Fixtures: doctor response shapes
// ---------------------------------------------------------------------------
const postgresCheckResponse = buildResponse('database connection failing', [
doctorCard({
checkId: 'check.postgres.connectivity',
title: 'Postgres Connectivity',
snippet: 'Validates TCP/TLS connectivity to the primary Postgres cluster.',
}),
doctorCard({
checkId: 'check.postgres.pool',
title: 'Postgres Connection Pool',
snippet: 'Connection pool utilization is within healthy thresholds.',
score: 0.72,
}),
doctorCard({
checkId: 'check.postgres.migrations',
title: 'Postgres Migration Status',
snippet: 'All EF Core migrations have been applied successfully.',
score: 0.65,
}),
], {
summary: 'Found 3 doctor checks related to database connectivity. Run `stella doctor run check.postgres.*` to diagnose.',
template: 'doctor_check',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['knowledge'],
});
const timestampingCheckResponse = buildResponse('TSA certificate expiry', [
doctorCard({
checkId: 'check.timestamp.tsa.certificate-expiry',
title: 'TSA Certificate Expiry',
snippet: 'Checks if the TSA signing certificate is approaching expiration.',
}),
doctorCard({
checkId: 'check.timestamp.tsa.chain-valid',
title: 'TSA Chain Valid',
snippet: 'Validates the full certificate chain from leaf to root CA.',
score: 0.78,
}),
doctorCard({
checkId: 'check.timestamp.tsa.root-expiry',
title: 'TSA Root Expiry',
snippet: 'Checks if the root CA certificate is approaching expiration.',
score: 0.70,
}),
], {
summary: 'Found 3 timestamping certificate checks. TSA infrastructure health is critical for eIDAS compliance.',
template: 'doctor_check',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['knowledge'],
});
const integrationCheckResponse = buildResponse('OCI registry connectivity', [
doctorCard({
checkId: 'check.integration.oci.registry',
title: 'OCI Registry',
snippet: 'Non-destructive HEAD probe to verify OCI registry availability.',
}),
doctorCard({
checkId: 'check.integration.oci.referrers',
title: 'OCI Registry Referrers API',
snippet: 'Checks if the registry supports the OCI Referrers API for artifact linking.',
score: 0.75,
}),
doctorCard({
checkId: 'check.integration.oci.credentials',
title: 'OCI Registry Credentials',
snippet: 'Validates stored registry credentials have not expired.',
score: 0.68,
}),
]);
const binaryAnalysisCheckResponse = buildResponse('debuginfod available', [
doctorCard({
checkId: 'check.binaryanalysis.debuginfod.available',
title: 'Debuginfod Availability',
snippet: 'Verifies debuginfod server is reachable for symbol resolution.',
}),
doctorCard({
checkId: 'check.binaryanalysis.buildinfo.cache',
title: 'Buildinfo Cache',
snippet: 'Checks buildinfo cache hit rate and staleness.',
score: 0.72,
}),
]);
const observabilityCheckResponse = buildResponse('OTLP endpoint check', [
doctorCard({
checkId: 'check.telemetry.otlp.endpoint',
title: 'OTLP Endpoint Check',
snippet: 'Validates the OpenTelemetry collector endpoint is accepting spans.',
}),
doctorCard({
checkId: 'check.metrics.prometheus.scrape',
title: 'Prometheus Scrape Check',
snippet: 'Verifies Prometheus scrape target is healthy and returning metrics.',
score: 0.70,
}),
]);
const complianceCheckResponse = buildResponse('audit readiness check', [
doctorCard({
checkId: 'check.compliance.audit-readiness',
title: 'Audit Readiness',
snippet: 'All evidence chains, attestations, and provenance records are complete for audit.',
}),
doctorCard({
checkId: 'check.compliance.evidence-integrity',
title: 'Evidence Tamper Check',
snippet: 'Verifies evidence locker integrity via Merkle tree verification.',
score: 0.82,
}),
doctorCard({
checkId: 'check.compliance.provenance-completeness',
title: 'Provenance Completeness',
snippet: 'Checks that all artifacts have complete provenance chains.',
score: 0.75,
}),
]);
const agentCheckResponse = buildResponse('agent heartbeat freshness', [
doctorCard({
checkId: 'check.agent.heartbeat.freshness',
title: 'Agent Heartbeat Freshness',
snippet: 'Checks that all registered agents have reported within the configured interval.',
}),
doctorCard({
checkId: 'check.agent.cluster.quorum',
title: 'Agent Cluster Quorum',
snippet: 'Validates that the agent cluster has sufficient members for quorum.',
score: 0.78,
}),
]);
const cryptoCheckResponse = buildResponse('FIPS compliance check', [
doctorCard({
checkId: 'check.crypto.fips',
title: 'FIPS 140-2 Compliance',
snippet: 'Verifies FIPS-validated cryptographic modules are loaded and active.',
}),
doctorCard({
checkId: 'check.crypto.hsm',
title: 'HSM PKCS#11 Availability',
snippet: 'Checks HSM module connectivity and signing key availability.',
score: 0.80,
}),
]);
const scannerCheckResponse = buildResponse('scanner queue check', [
doctorCard({
checkId: 'check.scanner.queue',
title: 'Scanner Queue Health',
snippet: 'Scanner job queue depth and processing rate are within normal thresholds.',
}),
doctorCard({
checkId: 'check.scanner.resources',
title: 'Scanner Resource Utilization',
snippet: 'CPU and memory usage for scanner workers are within configured limits.',
score: 0.74,
}),
]);
const releaseCheckResponse = buildResponse('promotion gates check', [
doctorCard({
checkId: 'check.release.promotion.gates',
title: 'Promotion Gate Health',
snippet: 'All promotion gates are correctly configured and responding.',
}),
doctorCard({
checkId: 'check.release.rollback.readiness',
title: 'Rollback Readiness',
snippet: 'Rollback procedures are verified and ready for execution.',
score: 0.72,
}),
]);
const vexCheckResponse = buildResponse('VEX schema compliance check', [
doctorCard({
checkId: 'check.vex.schema',
title: 'VEX Schema Compliance',
snippet: 'Validates all stored VEX documents conform to their declared schema.',
}),
doctorCard({
checkId: 'check.vex.issuer-trust',
title: 'VEX Issuer Trust',
snippet: 'Checks that VEX issuer trust tiers are configured and up to date.',
score: 0.73,
}),
]);
// ---------------------------------------------------------------------------
// Test suites
// ---------------------------------------------------------------------------
test.describe('Unified Search — Doctor Domain', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 3.1 Database & Infrastructure Checks
// -------------------------------------------------------------------------
test.describe('Database & Infrastructure Checks', () => {
const queries = [
'check.postgres.connectivity',
'database connection failing',
'postgres migrations pending',
'connection pool exhausted',
'disk space running low',
'evidence locker write check',
'backup directory writable',
];
for (const query of queries) {
test(`"${query}" returns doctor entity cards`, async ({ page }) => {
await mockSearchApi(page, postgresCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
const cards = await waitForEntityCards(page);
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(1);
// At least one card should have doctor-related content
const allText = await page.locator('.search__results').textContent();
expect(allText).toBeTruthy();
});
}
});
// -------------------------------------------------------------------------
// 3.2 Security & Auth Checks
// -------------------------------------------------------------------------
test.describe('Security & Auth Checks', () => {
const queries = [
'authentication config check',
'OIDC provider connectivity',
'signing key health',
'token service health',
'certificate chain validation',
'FIPS compliance check',
'HSM availability check',
'eIDAS compliance check',
'GOST availability check',
];
for (const query of queries) {
test(`"${query}" returns doctor cards`, async ({ page }) => {
const fixture = query.includes('FIPS') || query.includes('HSM') || query.includes('eIDAS') || query.includes('GOST')
? cryptoCheckResponse
: postgresCheckResponse;
await mockSearchApi(page, fixture);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.3 Compliance, Agent & Notification Checks
// -------------------------------------------------------------------------
test.describe('Compliance, Agent & Notification Checks', () => {
const complianceQueries = [
'audit readiness check',
'evidence integrity check',
'provenance completeness',
'attestation signing health',
'evidence generation rate',
'export readiness check',
'compliance framework check',
];
for (const query of complianceQueries) {
test(`compliance: "${query}"`, async ({ page }) => {
await mockSearchApi(page, complianceCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const agentQueries = [
'agent heartbeat freshness',
'agent capacity check',
'stale agent detection',
'agent cluster health',
'agent cluster quorum',
'agent version consistency',
'agent certificate expiry',
'agent task backlog',
];
for (const query of agentQueries) {
test(`agent: "${query}"`, async ({ page }) => {
await mockSearchApi(page, agentCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const notifyQueries = [
'email notification check',
'Slack connectivity check',
'Teams notification check',
'notification queue health',
'webhook connectivity',
];
for (const query of notifyQueries) {
test(`notify: "${query}"`, async ({ page }) => {
await mockSearchApi(page, integrationCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.4 Environment & Release Checks
// -------------------------------------------------------------------------
test.describe('Environment & Release Checks', () => {
const queries = [
'environment connectivity',
'environment drift',
'network policy enforcement',
'deployment health check',
'active release health',
'promotion gates check',
'rollback readiness',
'release schedule check',
];
for (const query of queries) {
test(`"${query}" returns cards`, async ({ page }) => {
await mockSearchApi(page, releaseCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.5 Timestamping & Certificate Lifecycle Checks
// -------------------------------------------------------------------------
test.describe('Timestamping & Certificate Lifecycle Checks', () => {
const queries = [
'TSA availability check',
'TSA response time',
'TSA valid response check',
'TSA failover ready',
'TSA certificate expiry',
'TSA root expiry check',
'TSA chain validation',
'OCSP responder check',
'CRL distribution check',
'revocation cache freshness',
'OCSP stapling enabled',
'evidence staleness check',
'timestamp approaching expiry',
'TST algorithm deprecated',
'retimestamp pending',
'EU trust list freshness',
'QTS providers qualified',
'system time synced',
];
for (const query of queries) {
test(`"${query}" returns timestamping doctor cards`, async ({ page }) => {
await mockSearchApi(page, timestampingCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.6 Integration & External Connectivity Checks
// -------------------------------------------------------------------------
test.describe('Integration & External Connectivity Checks', () => {
const queries = [
'OCI registry connectivity',
'OCI referrers API check',
'OCI push authorization',
'OCI pull authorization',
'OCI registry credentials',
'S3 object storage check',
'SMTP connectivity check',
'Slack webhook check',
'Teams webhook check',
'Git provider connectivity',
'LDAP connectivity check',
'CI system connectivity',
'secrets manager connectivity',
'integration webhook health',
'cannot push policy to OCI',
'Git provider auth failing',
'object storage write failing',
'secrets vault unreachable',
];
for (const query of queries) {
test(`"${query}" returns integration doctor cards`, async ({ page }) => {
await mockSearchApi(page, integrationCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.7 Binary Analysis & Corpus Health Checks
// -------------------------------------------------------------------------
test.describe('Binary Analysis & Corpus Health Checks', () => {
const queries = [
'debuginfod available',
'ddeb repo enabled',
'buildinfo cache health',
'symbol recovery fallback',
'corpus mirror freshness',
'corpus KPI baseline exists',
'binary analysis not working',
'symbol table missing',
'debug symbols not found',
'buildinfo cache expired',
'Go binary stripped no debug',
'PE authenticode verification failed',
'corpus mirror out of date',
];
for (const query of queries) {
test(`"${query}" returns binary analysis doctor cards`, async ({ page }) => {
await mockSearchApi(page, binaryAnalysisCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.8 Observability, Logging & Operations Deep Checks
// -------------------------------------------------------------------------
test.describe('Observability & Operations Checks', () => {
const queries = [
'OTLP exporter not sending',
'log directory not writable',
'log rotation not configured',
'Prometheus not scraping metrics',
'dead letter queue growing',
'job queue backlog increasing',
'scheduler not processing',
'traces not appearing in Jaeger',
'metrics endpoint 404',
'OpenTelemetry collector down',
'dead letter messages accumulating',
];
for (const query of queries) {
test(`"${query}" returns observability doctor cards`, async ({ page }) => {
await mockSearchApi(page, observabilityCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 3.9 Scanner, Reachability & Storage Deep Checks
// -------------------------------------------------------------------------
test.describe('Scanner & Storage Deep Checks', () => {
const queries = [
'scanner queue backed up',
'SBOM generation failing',
'vulnerability scan timing out',
'witness graph corruption',
'slice cache miss rate high',
'reachability computation stalled',
'scanner resource utilization high',
'disk space critical on evidence locker',
'evidence locker write failure',
'postgres connection pool exhausted',
'database migrations not applied',
];
for (const query of queries) {
test(`"${query}" returns scanner/storage doctor cards`, async ({ page }) => {
await mockSearchApi(page, scannerCheckResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// Doctor-specific rendering verification
// -------------------------------------------------------------------------
test('doctor cards show "Run Check" action button', async ({ page }) => {
await mockSearchApi(page, postgresCheckResponse);
await waitForShell(page);
await typeInSearch(page, 'database connection');
await waitForResults(page);
await waitForEntityCards(page, 3);
const allText = await page.locator('.search__results').textContent();
expect(allText).toContain('Run Check');
});
test('doctor synthesis uses doctor_check template', async ({ page }) => {
await mockSearchApi(page, postgresCheckResponse);
await waitForShell(page);
await typeInSearch(page, 'postgres connectivity');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('stella doctor run');
});
test('VEX doctor checks return VEX-specific cards', async ({ page }) => {
await mockSearchApi(page, vexCheckResponse);
await waitForShell(page);
await typeInSearch(page, 'VEX schema compliance');
await waitForResults(page);
await waitForEntityCards(page);
const allText = await page.locator('.search__results').textContent();
expect(allText).toContain('VEX Schema');
});
});

View File

@@ -0,0 +1,618 @@
// -----------------------------------------------------------------------------
// unified-search-findings.e2e.spec.ts
// E2E tests for Findings domain queries (200 cases from test-cases.md).
// Verifies: CVE entity cards, severity badges, PURL extraction, GHSA,
// secret detection, reachability, binary analysis, triage workflow.
// -----------------------------------------------------------------------------
import { expect, test } from '@playwright/test';
import {
buildResponse,
docsCard,
findingCard,
secretCard,
mockSearchApi,
setupAuthenticatedSession,
setupBasicMocks,
typeInSearch,
waitForEntityCards,
waitForResults,
waitForShell,
} from './unified-search-fixtures';
// ---------------------------------------------------------------------------
// Fixtures: findings response shapes
// ---------------------------------------------------------------------------
const cveSearchResponse = buildResponse('CVE-2024-21626', [
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626: Container Escape via runc',
snippet: 'A container escape vulnerability in runc allows attackers to escape container isolation.',
severity: 'critical',
score: 0.97,
}),
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626 in pkg:golang/runc@1.1.10',
snippet: 'Affected package: runc 1.1.10. Fixed in 1.1.12.',
severity: 'critical',
score: 0.90,
}),
], {
summary: 'CVE-2024-21626: CRITICAL container escape in runc. 2 affected packages found. Exploit available.',
template: 'cve_summary',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['findings'],
});
const highSeverityResponse = buildResponse('high severity findings', [
findingCard({
cveId: 'CVE-2024-0056',
title: 'CVE-2024-0056: .NET SQL Injection',
snippet: '.NET data provider allows SQL injection via crafted connection strings.',
severity: 'high',
score: 0.88,
}),
findingCard({
cveId: 'CVE-2023-38545',
title: 'CVE-2023-38545: curl SOCKS5 Overflow',
snippet: 'Heap-based buffer overflow in curl SOCKS5 proxy handshake.',
severity: 'high',
score: 0.85,
}),
findingCard({
cveId: 'CVE-2023-32233',
title: 'CVE-2023-32233: Linux kernel nf_tables',
snippet: 'Use-after-free in Linux kernel nf_tables allows local privilege escalation.',
severity: 'high',
score: 0.82,
}),
]);
const purlSearchResponse = buildResponse('pkg:npm/lodash@4.17.21', [
findingCard({
cveId: 'CVE-2021-23337',
title: 'CVE-2021-23337: lodash Command Injection',
snippet: 'lodash template function allows command injection via template strings.',
severity: 'high',
score: 0.91,
}),
findingCard({
cveId: 'CVE-2020-28500',
title: 'CVE-2020-28500: lodash ReDoS',
snippet: 'Regular expression denial of service in lodash.trimEnd.',
severity: 'medium',
score: 0.78,
}),
], {
summary: 'pkg:npm/lodash@4.17.21: 2 known vulnerabilities. 1 HIGH, 1 MEDIUM.',
template: 'package_summary',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['findings'],
});
const ghsaSearchResponse = buildResponse('GHSA-xxxx-yyyy-zzzz', [
findingCard({
cveId: 'GHSA-xxxx-yyyy-zzzz',
title: 'GHSA-xxxx-yyyy-zzzz: Remote Code Execution',
snippet: 'GitHub Security Advisory for remote code execution vulnerability.',
severity: 'critical',
score: 0.93,
}),
]);
const secretDetectionResponse = buildResponse('AWS access key exposed', [
secretCard({
title: 'AWS Access Key Exposed',
snippet: 'AWS Access Key ID AKIA*** detected in src/config/settings.ts:42',
severity: 'critical',
score: 0.96,
}),
secretCard({
title: 'AWS Secret Access Key',
snippet: 'AWS Secret Access Key detected adjacent to Access Key ID.',
severity: 'critical',
score: 0.94,
}),
], {
summary: 'CRITICAL: 2 AWS credential detections. Immediate key rotation required.',
template: 'secret_detection',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['findings'],
});
const reachabilityResponse = buildResponse('reachable CVE findings', [
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626 (REACHABLE)',
snippet: 'Static + runtime analysis confirms vulnerable code path is reachable from container entrypoint.',
severity: 'critical',
score: 0.98,
}),
findingCard({
cveId: 'CVE-2023-44487',
title: 'CVE-2023-44487 (REACHABLE)',
snippet: 'HTTP/2 Rapid Reset — runtime OTel trace confirms active HTTP/2 usage.',
severity: 'high',
score: 0.90,
}),
], {
summary: '2 REACHABLE findings. Both confirmed by runtime observation. Prioritize remediation.',
template: 'reachability_summary',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['findings'],
});
const binaryAnalysisResponse = buildResponse('stripped Go binary vulnerability', [
findingCard({
cveId: 'CVE-2024-24790',
title: 'CVE-2024-24790 in Go binary (stripped)',
snippet: 'net/netip ParseAddr vulnerability detected in stripped Go binary via symbol reconstruction.',
severity: 'high',
score: 0.84,
}),
]);
const triageWorkflowResponse = buildResponse('findings in active triage', [
findingCard({
cveId: 'CVE-2024-21626',
title: 'CVE-2024-21626 — Triage: ACTIVE',
snippet: 'Container escape vulnerability under active investigation. Assigned to security team.',
severity: 'critical',
score: 0.95,
}),
findingCard({
cveId: 'CVE-2023-44487',
title: 'CVE-2023-44487 — Triage: BLOCKED',
snippet: 'HTTP/2 Rapid Reset — blocking shipment until VEX or patch applied.',
severity: 'high',
score: 0.88,
}),
findingCard({
cveId: 'CVE-2021-44228',
title: 'CVE-2021-44228 — Triage: MUTED (VEX)',
snippet: 'Log4Shell muted by authoritative vendor VEX: component_not_present.',
severity: 'critical',
score: 0.80,
}),
], {
summary: '3 findings in triage: 1 ACTIVE, 1 BLOCKED, 1 MUTED. Review blocked findings for release gate.',
template: 'triage_summary',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['findings'],
});
const cweSearchResponse = buildResponse('SQL injection vulnerability', [
findingCard({
cveId: 'CVE-2024-0056',
title: 'CVE-2024-0056: SQL Injection (CWE-89)',
snippet: 'SQL injection via .NET data provider connection string manipulation.',
severity: 'high',
}),
]);
const namedVulnResponse = buildResponse('Log4Shell', [
findingCard({
cveId: 'CVE-2021-44228',
title: 'Log4Shell (CVE-2021-44228)',
snippet: 'Remote code execution in Apache Log4j 2.x via JNDI lookup injection.',
severity: 'critical',
score: 0.99,
}),
findingCard({
cveId: 'CVE-2021-45046',
title: 'Log4j followup (CVE-2021-45046)',
snippet: 'Incomplete fix for CVE-2021-44228 in certain non-default configurations.',
severity: 'critical',
score: 0.85,
}),
]);
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Findings Domain', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 4.1 CVE Searches
// -------------------------------------------------------------------------
test.describe('CVE Searches', () => {
const cveQueries = [
'CVE-2024-21626',
'CVE-2024-3094',
'CVE-2023-44487',
'CVE-2021-44228',
'CVE-2024-6387',
'CVE-2023-4863',
'CVE-2024-0056',
'CVE-2023-38545',
];
for (const query of cveQueries) {
test(`"${query}" returns finding entity cards`, async ({ page }) => {
await mockSearchApi(page, cveSearchResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
const cards = await waitForEntityCards(page);
const count = await cards.count();
expect(count).toBeGreaterThanOrEqual(1);
// Cards should show in findings domain
const firstCard = cards.first();
await expect(firstCard).toBeVisible();
});
}
test('CVE card shows severity badge', async ({ page }) => {
await mockSearchApi(page, cveSearchResponse);
await waitForShell(page);
await typeInSearch(page, 'CVE-2024-21626');
await waitForResults(page);
await waitForEntityCards(page, 2);
// Look for severity indicator
const resultsText = await page.locator('.search__results').textContent();
expect(resultsText?.toLowerCase()).toContain('critical');
});
const namedVulnQueries = [
'Log4Shell',
'Heartbleed',
'Spring4Shell',
'Shellshock',
'POODLE',
];
for (const query of namedVulnQueries) {
test(`named vulnerability: "${query}"`, async ({ page }) => {
await mockSearchApi(page, namedVulnResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// CVE by severity / CVSS / EPSS
// -------------------------------------------------------------------------
test.describe('Severity & Score Searches', () => {
const queries = [
'critical vulnerabilities',
'high severity findings',
'CVSS score 9.8',
'CVSS greater than 7',
'exploit available',
'zero day vulnerability',
'EPSS score high',
];
for (const query of queries) {
test(`"${query}" returns severity-filtered findings`, async ({ page }) => {
await mockSearchApi(page, highSeverityResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// CWE-based searches
// -------------------------------------------------------------------------
test.describe('CWE & Weakness Type Searches', () => {
const cweQueries = [
'remote code execution',
'SQL injection vulnerability',
'buffer overflow',
'cross site scripting',
'privilege escalation',
'denial of service',
'path traversal',
'deserialization vulnerability',
'SSRF vulnerability',
'use after free',
'null pointer dereference',
'race condition',
];
for (const query of cweQueries) {
test(`"${query}" returns CWE-typed findings`, async ({ page }) => {
await mockSearchApi(page, cweSearchResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 4.2 PURL & Package Searches
// -------------------------------------------------------------------------
test.describe('PURL & Package Searches', () => {
const purlQueries = [
'pkg:npm/lodash@4.17.21',
'pkg:maven/org.apache.logging.log4j/log4j-core@2.17.0',
'pkg:pypi/django@4.2',
'pkg:cargo/tokio@1.28',
'pkg:golang/github.com/opencontainers/runc@1.1.10',
'pkg:nuget/Newtonsoft.Json@13.0.3',
'pkg:gem/actionpack@7.0',
'pkg:npm/express@4.18',
];
for (const query of purlQueries) {
test(`PURL: "${query}" returns package findings`, async ({ page }) => {
await mockSearchApi(page, purlSearchResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('PURL search shows package_summary synthesis', async ({ page }) => {
await mockSearchApi(page, purlSearchResponse);
await waitForShell(page);
await typeInSearch(page, 'pkg:npm/lodash@4.17.21');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('lodash');
});
const packageNameQueries = [
'npm lodash vulnerability',
'jackson-databind CVE',
'spring framework vulnerability',
'golang net/http vulnerability',
'python requests vulnerability',
'docker runc vulnerability',
'kubernetes vulnerability',
'nginx CVE',
'postgresql vulnerability',
'redis vulnerability',
];
for (const query of packageNameQueries) {
test(`package name: "${query}"`, async ({ page }) => {
await mockSearchApi(page, purlSearchResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 4.3 GHSA & Source Searches
// -------------------------------------------------------------------------
test.describe('GHSA & Advisory Source Searches', () => {
const queries = [
'GHSA-xxxx-yyyy-zzzz',
'GitHub advisory',
'NVD advisory',
'CISA advisory',
'Red Hat security advisory',
'Debian security advisory',
'Ubuntu security notice',
'advisories published today',
'recently discovered vulnerabilities',
];
for (const query of queries) {
test(`"${query}" returns advisory source findings`, async ({ page }) => {
await mockSearchApi(page, ghsaSearchResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 4.4 Secret Detection & Credential Findings
// -------------------------------------------------------------------------
test.describe('Secret Detection Searches', () => {
const secretQueries = [
'AWS access key exposed',
'GitHub personal access token',
'private SSH key in repository',
'database password hardcoded',
'Slack webhook URL leaked',
'Azure connection string exposed',
'Docker registry credentials',
'JWT secret key in code',
'Stripe API key leaked',
'Google Cloud service account key',
'npm auth token',
'PKCS#12 certificate with private key',
'environment file with secrets',
'Terraform state with credentials',
'Kubernetes secret in YAML',
'PGP private key committed',
'OAuth client secret exposed',
'encryption key in code',
'all secret detections this week',
];
for (const query of secretQueries) {
test(`secret: "${query}"`, async ({ page }) => {
await mockSearchApi(page, secretDetectionResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('secret detection synthesis shows rotation warning', async ({ page }) => {
await mockSearchApi(page, secretDetectionResponse);
await waitForShell(page);
await typeInSearch(page, 'AWS access key');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('CRITICAL');
});
});
// -------------------------------------------------------------------------
// 4.5 Reachability & Runtime Analysis Findings
// -------------------------------------------------------------------------
test.describe('Reachability Searches', () => {
const reachQueries = [
'reachable CVE findings',
'unreachable vulnerabilities',
'conditional reachability',
'unknown reachability status',
'static path analysis',
'runtime hit confirmed',
'runtime sink hit',
'static analysis confirmed by runtime',
'OTel trace confirms vulnerable path',
'hot symbol detected at runtime',
'vulnerable function in execute path',
'no callstack to vulnerable code',
'entry point to sink path',
'reachability proof document',
];
for (const query of reachQueries) {
test(`reachability: "${query}"`, async ({ page }) => {
await mockSearchApi(page, reachabilityResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('reachability synthesis confirms runtime observation', async ({ page }) => {
await mockSearchApi(page, reachabilityResponse);
await waitForShell(page);
await typeInSearch(page, 'reachable CVE');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('REACHABLE');
});
});
// -------------------------------------------------------------------------
// 4.6 Binary & Crypto Analysis Findings
// -------------------------------------------------------------------------
test.describe('Binary & Crypto Analysis Searches', () => {
const binaryQueries = [
'stripped Go binary vulnerability',
'Mach-O binary CVE',
'Windows PE vulnerability',
'Authenticode signature invalid',
'native library vulnerability',
'statically linked vulnerable code',
'shared library CVE',
'musl libc vulnerability',
'glibc vulnerability',
];
for (const query of binaryQueries) {
test(`binary: "${query}"`, async ({ page }) => {
await mockSearchApi(page, binaryAnalysisResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const cryptoQueries = [
'weak cipher algorithm detected',
'deprecated TLS version',
'insecure hash function MD5',
'SHA1 deprecation warning',
'RSA key too short',
'self-signed certificate in production',
'certificate about to expire',
];
for (const query of cryptoQueries) {
test(`crypto: "${query}"`, async ({ page }) => {
await mockSearchApi(page, binaryAnalysisResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 4.7 Triage Workflow & Status Searches
// -------------------------------------------------------------------------
test.describe('Triage Workflow Searches', () => {
const triageQueries = [
'findings in active triage',
'blocked shipment findings',
'findings needing exception',
'muted by reachability',
'muted by VEX status',
'compensated findings',
'ship verdict findings',
'block verdict findings',
'exception granted findings',
'pending scan results',
'running scans',
'failed scan results',
'findings without evidence',
'unresolved findings older than 30 days',
'findings blocking production release',
];
for (const query of triageQueries) {
test(`triage: "${query}"`, async ({ page }) => {
await mockSearchApi(page, triageWorkflowResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('triage synthesis shows lane summary', async ({ page }) => {
await mockSearchApi(page, triageWorkflowResponse);
await waitForShell(page);
await typeInSearch(page, 'findings in active triage');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('ACTIVE');
});
});
});

View File

@@ -0,0 +1,485 @@
// -----------------------------------------------------------------------------
// unified-search-fixtures.ts
// Shared mock data & helpers for all unified search e2e test suites.
// -----------------------------------------------------------------------------
import type { Page } from '@playwright/test';
import { policyAuthorSession } from '../../src/app/testing';
// ---------------------------------------------------------------------------
// Auth / config fixtures
// ---------------------------------------------------------------------------
export const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stella-ops-ui',
authorizeEndpoint: 'https://authority.local/connect/authorize',
tokenEndpoint: 'https://authority.local/connect/token',
logoutEndpoint: 'https://authority.local/connect/logout',
redirectUri: 'http://127.0.0.1:4400/auth/callback',
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
scope:
'openid profile email ui.read advisory:search advisory:read search:read findings:read vex:read policy:read health:read',
audience: 'https://scanner.local',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: 'https://authority.local',
scanner: 'https://scanner.local',
policy: 'https://policy.local',
concelier: 'https://concelier.local',
attestor: 'https://attestor.local',
gateway: 'https://gateway.local',
},
quickstartMode: true,
setup: 'complete',
};
export const oidcConfig = {
issuer: mockConfig.authority.issuer,
authorization_endpoint: mockConfig.authority.authorizeEndpoint,
token_endpoint: mockConfig.authority.tokenEndpoint,
jwks_uri: 'https://authority.local/.well-known/jwks.json',
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
};
export const shellSession = {
...policyAuthorSession,
scopes: [
...new Set([
...policyAuthorSession.scopes,
'ui.read',
'admin',
'advisory:search',
'advisory:read',
'search:read',
'findings:read',
'vex:read',
'policy:read',
'health:read',
]),
],
};
// ---------------------------------------------------------------------------
// Page setup helpers
// ---------------------------------------------------------------------------
export async function setupBasicMocks(page: Page) {
page.on('console', (message) => {
if (message.type() !== 'error') return;
const text = message.text();
if (text.includes('status of 404') || text.includes('HttpErrorResponse')) return;
console.log('[browser:error]', text);
});
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('**/platform/envsettings.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('https://authority.local/**', (route) => {
const url = route.request().url();
if (url.includes('/.well-known/openid-configuration')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
});
}
if (url.includes('/.well-known/jwks.json')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ keys: [] }),
});
}
if (url.includes('authorize')) {
return route.abort();
}
return route.fulfill({ status: 400, body: 'blocked' });
});
}
export async function setupAuthenticatedSession(page: Page) {
await page.addInitScript((stubSession) => {
(window as any).__stellaopsTestSession = stubSession;
}, shellSession);
}
export async function waitForShell(page: Page) {
await page.goto('/');
await page.locator('aside.sidebar').waitFor({ state: 'visible', timeout: 15_000 });
}
// ---------------------------------------------------------------------------
// Search interaction helpers
// ---------------------------------------------------------------------------
/** Intercepts unified search and replies with the given fixture. */
export async function mockSearchApi(page: Page, responseBody: unknown) {
await page.route('**/search/query**', (route) => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(responseBody),
});
});
}
/**
* Intercepts unified search and replies with a fixture chosen at
* runtime based on `q`/`query` from JSON body or URL params. This allows a single test
* to issue multiple different queries and receive different mock responses.
*/
export async function mockSearchApiDynamic(
page: Page,
responseMap: Record<string, unknown>,
fallback?: unknown,
) {
await page.route('**/search/query**', async (route) => {
try {
const request = route.request();
let q = '';
const rawPostData = request.postData();
if (rawPostData) {
try {
const body = JSON.parse(rawPostData);
q = String(body.q ?? body.query ?? '');
} catch {
q = '';
}
}
if (!q) {
const url = new URL(request.url());
q = String(url.searchParams.get('q') ?? url.searchParams.get('query') ?? '');
}
const key = Object.keys(responseMap).find((k) => q.toLowerCase().includes(k.toLowerCase()));
const payload = key ? responseMap[key] : (fallback ?? emptyResponse(q));
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(payload),
});
} catch {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(fallback ?? emptyResponse('')),
});
}
});
}
export async function typeInSearch(page: Page, query: string) {
const input = page.locator('app-global-search input[type="text"]');
await input.focus();
await input.fill(query);
return input;
}
export async function waitForResults(page: Page) {
await page.locator('.search__results').waitFor({ state: 'visible', timeout: 10_000 });
const loading = page.locator('.search__loading');
if ((await loading.count()) > 0) {
await loading.first().waitFor({ state: 'hidden', timeout: 8_000 }).catch(() => {
// Ignore timeout here; some result states intentionally do not render loading.
});
}
}
export async function waitForEntityCards(page: Page, count?: number) {
const cards = page.locator(
'.search__cards app-entity-card:visible, [role="list"] [role="listitem"]:visible, [role="listbox"] [role="option"]:visible',
);
const waitTimeoutMs = 8_000;
const waitForCards = async () => {
if (count !== undefined) {
await cards.nth(count - 1).waitFor({ state: 'visible', timeout: waitTimeoutMs });
} else {
await cards.first().waitFor({ state: 'visible', timeout: waitTimeoutMs });
}
};
try {
await waitForCards();
} catch (firstError) {
// Retry once by re-triggering the current query input in case the
// debounced search call was dropped under long stress runs.
try {
const input = page.locator('app-global-search input[type="text"]');
if ((await input.count()) > 0) {
const searchInput = input.first();
const current = await searchInput.inputValue();
if (current.trim().length >= 2) {
await searchInput.focus();
await searchInput.fill(current);
await searchInput.press(' ');
await searchInput.press('Backspace');
await searchInput.press('Enter').catch(() => {
// Some environments do not bind Enter on the input.
});
await waitForResults(page);
}
}
} catch {
// Ignore retry interaction errors and still attempt one more card wait.
}
try {
await waitForCards();
} catch {
throw firstError;
}
}
return cards;
}
// ---------------------------------------------------------------------------
// Mock response builders
// ---------------------------------------------------------------------------
export interface CardFixture {
entityKey: string;
entityType: string;
domain: string;
title: string;
snippet: string;
score: number;
severity?: string;
actions: Array<{
label: string;
actionType: string;
route?: string;
command?: string;
isPrimary: boolean;
}>;
sources: string[];
metadata?: Record<string, string>;
}
export function buildResponse(
query: string,
cards: CardFixture[],
synthesis?: {
summary: string;
template: string;
confidence: string;
sourceCount: number;
domainsCovered: string[];
},
) {
return {
query,
topK: 10,
cards,
synthesis: synthesis ?? {
summary: `Found ${cards.length} result(s) for "${query}".`,
template: cards.length > 0 ? 'mixed_overview' : 'empty',
confidence: cards.length >= 3 ? 'high' : cards.length >= 1 ? 'medium' : 'low',
sourceCount: cards.length,
domainsCovered: [...new Set(cards.map((c) => c.domain))],
},
diagnostics: {
ftsMatches: cards.length + 2,
vectorMatches: cards.length,
entityCardCount: cards.length,
durationMs: 38,
usedVector: true,
mode: 'hybrid',
},
};
}
export function emptyResponse(query: string) {
return buildResponse(query, [], {
summary: 'No results found for the given query.',
template: 'empty',
confidence: 'high',
sourceCount: 0,
domainsCovered: [],
});
}
// ---------------------------------------------------------------------------
// Domain-specific card factories
// ---------------------------------------------------------------------------
export function doctorCard(opts: {
checkId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `doctor:${opts.checkId}`,
entityType: 'doctor',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.85,
actions: [
{ label: 'Run Check', actionType: 'run', command: `stella doctor run ${opts.checkId}`, isPrimary: true },
{ label: 'View Docs', actionType: 'navigate', route: '/ops/operations/data-integrity', isPrimary: false },
],
sources: ['knowledge'],
};
}
export function findingCard(opts: {
cveId: string;
title: string;
snippet: string;
severity: string;
score?: number;
}): CardFixture {
return {
entityKey: `cve:${opts.cveId}`,
entityType: 'finding',
domain: 'findings',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.92,
severity: opts.severity,
actions: [
{ label: 'View Finding', actionType: 'navigate', route: `/security/triage?q=${opts.cveId}`, isPrimary: true },
{ label: 'Copy CVE', actionType: 'copy', command: opts.cveId, isPrimary: false },
],
sources: ['findings'],
};
}
export function secretCard(opts: {
title: string;
snippet: string;
severity: string;
score?: number;
}): CardFixture {
return {
entityKey: `secret:${opts.title.toLowerCase().replace(/\s+/g, '-')}`,
entityType: 'finding',
domain: 'findings',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.88,
severity: opts.severity,
actions: [
{ label: 'View Secret', actionType: 'navigate', route: '/security/triage?type=secret', isPrimary: true },
],
sources: ['findings'],
};
}
export function vexCard(opts: {
cveId: string;
status: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `vex:${opts.cveId}`,
entityType: 'vex_statement',
domain: 'vex',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.80,
actions: [
{
label: 'View VEX',
actionType: 'navigate',
route: `/security/advisories-vex?q=${opts.cveId}`,
isPrimary: true,
},
],
sources: ['vex'],
metadata: { status: opts.status },
};
}
export function policyCard(opts: {
ruleId: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `policy:${opts.ruleId}`,
entityType: 'policy_rule',
domain: 'policy',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.78,
actions: [
{
label: 'View Rule',
actionType: 'navigate',
route: `/ops/policy?rule=${opts.ruleId}`,
isPrimary: true,
},
{ label: 'Simulate', actionType: 'navigate', route: `/ops/policy/simulate`, isPrimary: false },
],
sources: ['policy'],
};
}
export function docsCard(opts: {
docPath: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `docs:${opts.docPath}`,
entityType: 'docs',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.70,
actions: [
{ label: 'Open', actionType: 'navigate', route: `/docs/${opts.docPath}`, isPrimary: true },
],
sources: ['knowledge'],
};
}
export function apiCard(opts: {
endpoint: string;
method: string;
title: string;
snippet: string;
score?: number;
}): CardFixture {
return {
entityKey: `api:${opts.method}:${opts.endpoint}`,
entityType: 'api',
domain: 'knowledge',
title: opts.title,
snippet: opts.snippet,
score: opts.score ?? 0.75,
actions: [
{ label: 'Try API', actionType: 'curl', command: `curl -X ${opts.method} ${opts.endpoint}`, isPrimary: true },
{ label: 'Open Docs', actionType: 'navigate', route: `/docs/api`, isPrimary: false },
],
sources: ['knowledge'],
};
}

View File

@@ -0,0 +1,828 @@
// -----------------------------------------------------------------------------
// unified-search-vex-policy.e2e.spec.ts
// E2E tests for VEX (100 cases) and Policy (100 cases) domain queries.
// Verifies: VEX status/justification/trust/consensus cards, policy gate/verdict
// cards, risk budget, sealed mode, unknowns, observation states.
// -----------------------------------------------------------------------------
import { expect, test } from '@playwright/test';
import {
buildResponse,
docsCard,
findingCard,
policyCard,
vexCard,
mockSearchApi,
setupAuthenticatedSession,
setupBasicMocks,
typeInSearch,
waitForEntityCards,
waitForResults,
waitForShell,
} from './unified-search-fixtures';
// ---------------------------------------------------------------------------
// VEX Fixtures
// ---------------------------------------------------------------------------
const vexStatusResponse = buildResponse('VEX not affected', [
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'VEX: CVE-2024-21626 — Not Affected',
snippet: 'Vendor confirms: vulnerable code not present in this build. Justification: component_not_present.',
score: 0.92,
}),
vexCard({
cveId: 'CVE-2023-44487',
status: 'not_affected',
title: 'VEX: CVE-2023-44487 — Not Affected',
snippet: 'HTTP/2 Rapid Reset not exploitable: vulnerable_code_not_in_execute_path.',
score: 0.85,
}),
], {
summary: '2 VEX statements with status "not_affected". Both with verified justifications.',
template: 'vex_summary',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['vex'],
});
const vexAffectedResponse = buildResponse('VEX affected', [
vexCard({
cveId: 'CVE-2024-3094',
status: 'affected',
title: 'VEX: CVE-2024-3094 — Affected',
snippet: 'XZ Utils backdoor confirmed affected. Upgrade to xz 5.6.1-2 required.',
score: 0.95,
}),
]);
const vexFixedResponse = buildResponse('VEX fixed', [
vexCard({
cveId: 'CVE-2021-44228',
status: 'fixed',
title: 'VEX: CVE-2021-44228 — Fixed',
snippet: 'Log4Shell fixed in log4j-core 2.17.1. All deployments updated.',
score: 0.88,
}),
]);
const vexUnderInvestigationResponse = buildResponse('VEX under investigation', [
vexCard({
cveId: 'CVE-2024-6387',
status: 'under_investigation',
title: 'VEX: CVE-2024-6387 — Under Investigation',
snippet: 'OpenSSH regreSSHion applicability being assessed across fleet.',
score: 0.80,
}),
]);
const vexJustificationResponse = buildResponse('vulnerable code not present', [
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'VEX: vulnerable_code_not_present',
snippet: 'Justification: The vulnerable code path in runc was removed in our fork.',
}),
vexCard({
cveId: 'CVE-2023-4863',
status: 'not_affected',
title: 'VEX: vulnerable_code_not_in_execute_path',
snippet: 'Justification: libwebp is linked but image decoding disabled at build time.',
score: 0.78,
}),
]);
const vexTrustResponse = buildResponse('authoritative VEX source', [
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'VEX from Docker Inc. (Authoritative)',
snippet: 'Trust tier: AUTHORITATIVE. Vendor PSIRT issued. DSSE signed. Verified.',
score: 0.96,
}),
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'VEX from Community (Trusted)',
snippet: 'Trust tier: TRUSTED. Community analysis. PGP signed. Verified.',
score: 0.72,
}),
], {
summary: '2 VEX sources for CVE-2024-21626. Authoritative vendor source agrees with community. High confidence.',
template: 'vex_trust',
confidence: 'high',
sourceCount: 2,
domainsCovered: ['vex'],
});
const vexConflictResponse = buildResponse('VEX consensus conflict', [
vexCard({
cveId: 'CVE-2024-3094',
status: 'affected',
title: 'VEX Conflict: Vendor says AFFECTED',
snippet: 'Vendor PSIRT: affected. Trust tier: Authoritative.',
score: 0.90,
}),
vexCard({
cveId: 'CVE-2024-3094',
status: 'not_affected',
title: 'VEX Conflict: Community says NOT_AFFECTED',
snippet: 'Community analysis: not_affected. Trust tier: Trusted. Justification: component_not_present.',
score: 0.75,
}),
], {
summary: 'HARD CONFLICT: Vendor (affected) vs Community (not_affected) for CVE-2024-3094. Manual review required.',
template: 'vex_conflict',
confidence: 'low',
sourceCount: 2,
domainsCovered: ['vex'],
});
const vexFormatResponse = buildResponse('OpenVEX document', [
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'OpenVEX: CVE-2024-21626',
snippet: 'Format: OpenVEX v0.2. DSSE-signed. Schema validated.',
}),
]);
const vexWorkflowResponse = buildResponse('generate VEX document', [
vexCard({
cveId: 'CVE-2024-21626',
status: 'not_affected',
title: 'Generated VEX for CVE-2024-21626',
snippet: 'Run: stella vex generate --cve CVE-2024-21626 --status not_affected',
}),
docsCard({
docPath: 'VEX_CONSENSUS_GUIDE.md',
title: 'VEX Consensus Guide',
snippet: 'Guide for creating and managing VEX documents in Stella Ops.',
score: 0.65,
}),
]);
// ---------------------------------------------------------------------------
// Policy Fixtures
// ---------------------------------------------------------------------------
const policyManagementResponse = buildResponse('create policy rule', [
policyCard({
ruleId: 'severity-gate-critical',
title: 'Block Critical Vulnerabilities',
snippet: 'Blocks release promotion when CRITICAL severity findings are present without VEX.',
score: 0.90,
}),
policyCard({
ruleId: 'sbom-attestation-required',
title: 'Require SBOM Attestation',
snippet: 'Requires signed SBOM attestation predicate before promotion to production.',
score: 0.82,
}),
docsCard({
docPath: 'modules/policy/architecture.md',
title: 'Policy Engine Architecture',
snippet: 'Policy rules, packs, gates, and evaluation pipeline documentation.',
score: 0.65,
}),
], {
summary: '2 policy rules and 1 documentation result. Use Policy Studio to create or edit rules.',
template: 'policy_rule',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['policy', 'knowledge'],
});
const policyGateResponse = buildResponse('VEX trust gate evaluation', [
policyCard({
ruleId: 'vex-trust-gate',
title: 'VEX Trust Gate',
snippet: 'Validates VEX source trust tier meets minimum threshold before accepting as evidence.',
score: 0.92,
}),
policyCard({
ruleId: 'reachable-cve-gate',
title: 'Reachable CVE Gate',
snippet: 'Blocks if CVE is reachable and VEX status is "affected" or missing.',
score: 0.85,
}),
policyCard({
ruleId: 'execution-evidence-gate',
title: 'Execution Evidence Gate',
snippet: 'Requires signed execution evidence (SEE-03) for all promoted artifacts.',
score: 0.80,
}),
], {
summary: '3 policy gates found. VexTrustGate, ReachableCveGate, and ExecutionEvidenceGate.',
template: 'policy_gate',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['policy'],
});
const policyVerdictResponse = buildResponse('policy verdict blocked', [
policyCard({
ruleId: 'verdict-block-cve-2024',
title: 'Verdict: BLOCKED — CVE-2024-21626',
snippet: 'ReachableCveGate blocked promotion. Finding is reachable with no VEX justification.',
score: 0.95,
}),
policyCard({
ruleId: 'verdict-guarded-cve-2023',
title: 'Verdict: GUARDED PASS — CVE-2023-44487',
snippet: 'GuardedPass with runtime monitoring. Beacon verification rate: 94%.',
score: 0.82,
}),
]);
const policyRiskBudgetResponse = buildResponse('risk budget remaining', [
policyCard({
ruleId: 'risk-budget-project-alpha',
title: 'Risk Budget: Project Alpha',
snippet: 'Budget: 45/100 points consumed. 55 remaining. Burn rate: 3.2 points/week.',
score: 0.88,
}),
policyCard({
ruleId: 'unknowns-budget',
title: 'Unknowns Budget',
snippet: 'Unknowns: 12/20 allowed. U-RCH: 5, U-VEX: 3, U-FEED: 2, U-PROV: 2.',
score: 0.80,
}),
]);
const policyObservationResponse = buildResponse('observation pending determinization', [
policyCard({
ruleId: 'obs-pending-det',
title: 'Observation: Pending Determinization',
snippet: 'CVE discovered but evidence incomplete. Guardrail-based evaluation active.',
score: 0.85,
}),
policyCard({
ruleId: 'obs-disputed',
title: 'Observation: Disputed',
snippet: 'Multiple signals conflict. Runtime says reachable, static says unreachable. Human review required.',
score: 0.78,
}),
policyCard({
ruleId: 'obs-stale',
title: 'Observation: Stale Requires Refresh',
snippet: 'Evidence decayed past TTL threshold. Auto-triggered refresh in progress.',
score: 0.72,
}),
]);
const policySealedModeResponse = buildResponse('sealed mode locked dependencies', [
policyCard({
ruleId: 'sealed-mode-config',
title: 'Sealed Mode Configuration',
snippet: 'Dependencies locked, evidence frozen, no external network calls during evaluation.',
score: 0.90,
}),
docsCard({
docPath: 'contracts/sealed-mode.md',
title: 'Sealed Mode Contract',
snippet: 'Deterministic, offline-first evaluation mode for air-gap environments.',
score: 0.75,
}),
]);
const policyGateLevelResponse = buildResponse('G3 high risk gate level', [
policyCard({
ruleId: 'g3-high-risk',
title: 'G3 — High Risk Gate',
snippet: 'Requires: security scan, migration plan, load checks, observability updates, release captain sign-off.',
score: 0.88,
}),
]);
const policyDecisionResponse = buildResponse('why was release blocked', [
policyCard({
ruleId: 'decision-audit-release-42',
title: 'Release #42 Decision Audit',
snippet: 'BLOCKED by ReachableCveGate: CVE-2024-21626 is reachable. No VEX. No exception. Risk budget exceeded.',
score: 0.95,
}),
]);
// ---------------------------------------------------------------------------
// VEX Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — VEX Domain', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 5.1 VEX Status & Justification Searches
// -------------------------------------------------------------------------
test.describe('VEX Status Searches', () => {
const statusTests: Array<{ query: string; fixture: unknown }> = [
{ query: 'VEX not affected', fixture: vexStatusResponse },
{ query: 'VEX affected', fixture: vexAffectedResponse },
{ query: 'VEX fixed', fixture: vexFixedResponse },
{ query: 'VEX under investigation', fixture: vexUnderInvestigationResponse },
];
for (const { query, fixture } of statusTests) {
test(`"${query}" returns VEX cards with correct status`, async ({ page }) => {
await mockSearchApi(page, fixture);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
const cards = await waitForEntityCards(page);
expect(await cards.count()).toBeGreaterThanOrEqual(1);
});
}
const justificationQueries = [
'component not present justification',
'vulnerable code not present',
'code not in execute path',
'code not executable',
'adversary cannot control code',
'inline mitigations exist',
];
for (const query of justificationQueries) {
test(`justification: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexJustificationResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const generalVexQueries = [
'VEX for CVE-2024-21626',
'VEX for log4j',
'VEX from vendor',
'VEX from community',
'trusted VEX statements',
'authoritative VEX',
'VEX impact statement',
'VEX action required',
'VEX expiring soon',
'VEX override',
];
for (const query of generalVexQueries) {
test(`general: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexStatusResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 5.2 VEX Format & Workflow
// -------------------------------------------------------------------------
test.describe('VEX Format & Workflow', () => {
const formatQueries = [
'OpenVEX document',
'CSAF VEX document',
'CycloneDX VEX',
'StellaOps canonical VEX',
'VEX from SPDX format',
];
for (const query of formatQueries) {
test(`format: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexFormatResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const workflowQueries = [
'generate VEX document',
'ingest VEX statement',
'VEX hub search',
'VEX studio create',
'export VEX bundle',
'VEX consensus handling',
'VEX linked to finding',
'VEX suppresses finding',
'VEX as evidence',
'VEX attestation',
'VEX policy evaluation',
'VEX mirror',
'VEX feed subscription',
'VEX document lifecycle',
];
for (const query of workflowQueries) {
test(`workflow: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexWorkflowResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 5.3 VEX Trust, Signature & Freshness
// -------------------------------------------------------------------------
test.describe('VEX Trust & Signature', () => {
const trustQueries = [
'authoritative VEX source',
'trusted community VEX',
'untrusted VEX statement',
'vendor PSIRT VEX',
'distributor VEX statement',
'community VEX source',
'internal organization VEX',
'aggregator VEX source',
'VEX with high trust score',
'VEX issuer reputation',
];
for (const query of trustQueries) {
test(`trust: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexTrustResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const signatureQueries = [
'DSSE signed VEX document',
'cosign verified VEX',
'PGP signed VEX statement',
'X.509 signed VEX document',
'unverified VEX signature',
'failed VEX signature verification',
];
for (const query of signatureQueries) {
test(`signature: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexTrustResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const freshnessQueries = [
'VEX freshness stale',
'VEX freshness expired',
'VEX superseded by newer',
'fresh VEX statements only',
'VEX document age over 90 days',
];
for (const query of freshnessQueries) {
test(`freshness: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexStatusResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 5.4 VEX Consensus & Conflict
// -------------------------------------------------------------------------
test.describe('VEX Consensus & Conflict', () => {
const conflictQueries = [
'VEX consensus conflict',
'hard conflict between VEX sources',
'soft conflict VEX disagreement',
'vendor says not_affected community says affected',
'VEX consensus engine result',
'trust-weighted VEX merge',
'VEX confidence score low',
'VEX confidence high agreement',
'multiple issuers same CVE',
];
for (const query of conflictQueries) {
test(`conflict: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexConflictResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('VEX conflict synthesis shows low confidence', async ({ page }) => {
await mockSearchApi(page, vexConflictResponse);
await waitForShell(page);
await typeInSearch(page, 'VEX consensus conflict');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
const text = await synthesis.textContent();
expect(text).toContain('CONFLICT');
});
const transitionQueries = [
'VEX status transition history',
'affected changed to not_affected',
'under_investigation resolved to fixed',
'VEX linked to SBOM component',
'VEX for CPE product match',
'VEX suppressing active finding',
'VEX impact on policy gate',
'VEX used as evidence in release',
'VEX document schema validation failure',
];
for (const query of transitionQueries) {
test(`transition: "${query}"`, async ({ page }) => {
await mockSearchApi(page, vexConflictResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
});
// ---------------------------------------------------------------------------
// Policy Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search — Policy Domain', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 6.1 Policy Management Searches
// -------------------------------------------------------------------------
test.describe('Policy Management', () => {
const queries = [
'create policy rule',
'policy pack install',
'validate policy YAML',
'policy simulation',
'push policy to OCI',
'pull policy from registry',
'policy pack bundle',
'block critical vulnerabilities',
'require SBOM attestation',
'require VEX for all CVEs',
'maximum CVSS score allowed',
'block exploit available',
'require reachability proof',
'policy for production environment',
'policy exception request',
'policy waiver',
'risk budget remaining',
'policy violation list',
'policy determinism verification',
'policy lint check',
];
for (const query of queries) {
test(`"${query}" returns policy cards`, async ({ page }) => {
await mockSearchApi(page, policyManagementResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 6.2 Policy Evaluation & Decisioning
// -------------------------------------------------------------------------
test.describe('Policy Evaluation', () => {
const queries = [
'evaluate policy for container',
'policy APPROVE decision',
'policy REJECT decision',
'conditional approval',
'blocked by policy',
'override policy violation',
'severity fusion scoring',
'attestation report for release',
'promotion gate evaluation',
'policy snapshot comparison',
];
for (const query of queries) {
test(`evaluation: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyVerdictResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
test('policy decision shows audit detail', async ({ page }) => {
await mockSearchApi(page, policyDecisionResponse);
await waitForShell(page);
await typeInSearch(page, 'why was release blocked');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
});
});
// -------------------------------------------------------------------------
// 6.3 Gate-Level Evaluation & Verdict Searches
// -------------------------------------------------------------------------
test.describe('Gate-Level Evaluation', () => {
const gateQueries = [
'VEX trust gate evaluation',
'reachable CVE gate blocked',
'execution evidence gate result',
'beacon rate gate threshold',
'drift gate unreviewed changes',
'unknowns gate budget exceeded',
];
for (const query of gateQueries) {
test(`gate: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyGateResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const verdictQueries = [
'policy verdict pass',
'policy verdict guarded pass',
'policy verdict blocked',
'policy verdict ignored',
'policy verdict warned',
'policy verdict deferred',
'policy verdict escalated',
'policy verdict requires VEX',
];
for (const query of verdictQueries) {
test(`verdict: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyVerdictResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const gateLevelQueries = [
'G0 no-risk gate level',
'G1 low risk gate level',
'G2 moderate risk gate level',
'G3 high risk gate level',
'G4 safety critical gate level',
];
for (const query of gateLevelQueries) {
test(`gate level: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyGateLevelResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// 6.4 Risk Budget, Unknowns & Sealed Mode
// -------------------------------------------------------------------------
test.describe('Risk Budget & Unknowns', () => {
const budgetQueries = [
'risk budget remaining for project',
'risk budget burn rate',
'unknowns budget exceeded',
];
for (const query of budgetQueries) {
test(`budget: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyRiskBudgetResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const unknownCodeQueries = [
'unknown reachability reason',
'unknown identity ambiguous package',
'unknown provenance cannot map binary',
'VEX conflict unknown',
'feed gap unknown source missing',
'config unknown feature not observable',
'analyzer limit language not supported',
];
for (const query of unknownCodeQueries) {
test(`unknown code: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyRiskBudgetResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const observationQueries = [
'observation pending determinization',
'observation determined',
'observation disputed',
'observation stale requires refresh',
'observation manual review required',
'observation suppressed',
];
for (const query of observationQueries) {
test(`observation: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyObservationResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const sealedModeQueries = [
'sealed mode locked dependencies',
'sealed mode frozen evidence',
'deterministic replay manifest',
'no external network during evaluation',
];
for (const query of sealedModeQueries) {
test(`sealed mode: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policySealedModeResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
const tierQueries = [
'uncertainty tier T1',
'uncertainty tier T2',
'uncertainty tier T3',
'uncertainty tier T4',
];
for (const query of tierQueries) {
test(`uncertainty: "${query}"`, async ({ page }) => {
await mockSearchApi(page, policyObservationResponse);
await waitForShell(page);
await typeInSearch(page, query);
await waitForResults(page);
await waitForEntityCards(page);
});
}
});
// -------------------------------------------------------------------------
// Policy-specific rendering
// -------------------------------------------------------------------------
test('policy cards show "Simulate" action', async ({ page }) => {
await mockSearchApi(page, policyManagementResponse);
await waitForShell(page);
await typeInSearch(page, 'block critical vulnerabilities');
await waitForResults(page);
await waitForEntityCards(page);
const allText = await page.locator('.search__results').textContent();
expect(allText).toContain('Simulate');
});
test('risk budget synthesis shows budget numbers', async ({ page }) => {
await mockSearchApi(page, policyRiskBudgetResponse);
await waitForShell(page);
await typeInSearch(page, 'risk budget remaining');
await waitForResults(page);
const synthesis = page.locator('app-synthesis-panel');
await expect(synthesis).toBeVisible({ timeout: 10_000 });
});
});

View File

@@ -0,0 +1,493 @@
// -----------------------------------------------------------------------------
// unified-search.e2e.spec.ts
// E2E tests for the Unified Search feature.
// Tests search input, entity cards, domain filters, keyboard navigation,
// synthesis panel, empty state, and accessibility.
// -----------------------------------------------------------------------------
import { expect, test, type Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { policyAuthorSession } from '../../src/app/testing';
// ---------------------------------------------------------------------------
// Shared mock data
// ---------------------------------------------------------------------------
const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stella-ops-ui',
authorizeEndpoint: 'https://authority.local/connect/authorize',
tokenEndpoint: 'https://authority.local/connect/token',
logoutEndpoint: 'https://authority.local/connect/logout',
redirectUri: 'http://127.0.0.1:4400/auth/callback',
postLogoutRedirectUri: 'http://127.0.0.1:4400/',
scope:
'openid profile email ui.read advisory:search advisory:read search:read findings:read',
audience: 'https://scanner.local',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: 'https://authority.local',
scanner: 'https://scanner.local',
policy: 'https://policy.local',
concelier: 'https://concelier.local',
attestor: 'https://attestor.local',
gateway: 'https://gateway.local',
},
quickstartMode: true,
setup: 'complete',
};
const oidcConfig = {
issuer: mockConfig.authority.issuer,
authorization_endpoint: mockConfig.authority.authorizeEndpoint,
token_endpoint: mockConfig.authority.tokenEndpoint,
jwks_uri: 'https://authority.local/.well-known/jwks.json',
response_types_supported: ['code'],
subject_types_supported: ['public'],
id_token_signing_alg_values_supported: ['RS256'],
};
const shellSession = {
...policyAuthorSession,
scopes: [
...new Set([
...policyAuthorSession.scopes,
'ui.read',
'admin',
'advisory:search',
'advisory:read',
'search:read',
'findings:read',
'vex:read',
]),
],
};
// ---------------------------------------------------------------------------
// Mock API response fixtures
// ---------------------------------------------------------------------------
const searchResultsResponse = {
query: 'CVE-2024-21626',
topK: 10,
cards: [
{
entityKey: 'cve:CVE-2024-21626',
entityType: 'finding',
domain: 'findings',
title: 'CVE-2024-21626: Container Escape via runc',
snippet: 'A container escape vulnerability in runc allows...',
score: 0.95,
severity: 'critical',
actions: [
{
label: 'View Finding',
actionType: 'navigate',
route: '/security/triage?q=CVE-2024-21626',
isPrimary: true,
},
{
label: 'Copy CVE',
actionType: 'copy',
command: 'CVE-2024-21626',
isPrimary: false,
},
],
sources: ['findings'],
},
{
entityKey: 'vex:CVE-2024-21626',
entityType: 'vex_statement',
domain: 'vex',
title: 'VEX: CVE-2024-21626 - Not Affected',
snippet: 'Product not affected by CVE-2024-21626...',
score: 0.82,
actions: [
{
label: 'View VEX',
actionType: 'navigate',
route: '/security/advisories-vex?q=CVE-2024-21626',
isPrimary: true,
},
],
sources: ['vex'],
},
{
entityKey: 'docs:container-deployment',
entityType: 'docs',
domain: 'knowledge',
title: 'Container Deployment Guide',
snippet: 'Guide for deploying containers securely...',
score: 0.65,
actions: [
{
label: 'Open',
actionType: 'navigate',
route: '/docs/deploy.md#overview',
isPrimary: true,
},
],
sources: ['knowledge'],
},
],
synthesis: {
summary:
'Results for CVE-2024-21626: 1 finding, 1 VEX statement, 1 knowledge result. CRITICAL severity finding detected.',
template: 'cve_summary',
confidence: 'high',
sourceCount: 3,
domainsCovered: ['findings', 'vex', 'knowledge'],
},
diagnostics: {
ftsMatches: 5,
vectorMatches: 3,
entityCardCount: 3,
durationMs: 42,
usedVector: true,
mode: 'hybrid',
},
};
const emptySearchResponse = {
query: 'xyznonexistent999',
topK: 10,
cards: [],
synthesis: {
summary: 'No results found for the given query.',
template: 'empty',
confidence: 'high',
sourceCount: 0,
domainsCovered: [],
},
diagnostics: {
ftsMatches: 0,
vectorMatches: 0,
entityCardCount: 0,
durationMs: 5,
usedVector: true,
mode: 'hybrid',
},
};
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
async function setupBasicMocks(page: Page) {
page.on('console', (message) => {
if (message.type() === 'error') {
console.log('[browser:error]', message.text());
}
});
await page.route('**/config.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('**/platform/envsettings.json', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockConfig),
}),
);
await page.route('https://authority.local/**', (route) => {
const url = route.request().url();
if (url.includes('/.well-known/openid-configuration')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(oidcConfig),
});
}
if (url.includes('/.well-known/jwks.json')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ keys: [] }),
});
}
if (url.includes('authorize')) {
return route.abort();
}
return route.fulfill({ status: 400, body: 'blocked' });
});
}
async function setupAuthenticatedSession(page: Page) {
await page.addInitScript((stubSession) => {
(window as any).__stellaopsTestSession = stubSession;
}, shellSession);
}
/**
* Intercepts POST requests to the unified search endpoint and replies with
* the provided fixture payload.
*/
async function mockSearchApi(page: Page, responseBody: unknown) {
await page.route('**/search/query**', (route) => {
if (route.request().method() === 'POST') {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(responseBody),
});
}
return route.continue();
});
}
/**
* Focuses the global search input and types a query.
* Returns the search input locator for further assertions.
*/
async function typeInSearchInput(page: Page, query: string) {
const searchInput = page.locator('app-global-search input[type="text"]');
await searchInput.focus();
await searchInput.fill(query);
return searchInput;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test.describe('Unified Search', () => {
test.beforeEach(async ({ page }) => {
await setupBasicMocks(page);
await setupAuthenticatedSession(page);
});
// -------------------------------------------------------------------------
// 1. Search input is visible and focusable
// -------------------------------------------------------------------------
test('search input is visible and focusable', async ({ page }) => {
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
const searchInput = page.locator('app-global-search input[type="text"]');
await expect(searchInput).toBeVisible({ timeout: 10_000 });
await expect(searchInput).toHaveAttribute('placeholder', /search everything/i);
await searchInput.focus();
await expect(searchInput).toBeFocused();
});
// -------------------------------------------------------------------------
// 2. Typing a query shows entity cards
// -------------------------------------------------------------------------
test('typing a query shows entity cards with correct titles', async ({ page }) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
// Verify entity cards render
const entityCards = page.locator('app-entity-card');
await expect(entityCards).toHaveCount(3, { timeout: 10_000 });
// Verify card titles match the mock data
const cardTitles = await entityCards.allTextContents();
const combinedText = cardTitles.join(' ');
expect(combinedText).toContain('CVE-2024-21626');
expect(combinedText).toContain('Container Escape via runc');
expect(combinedText).toContain('VEX');
expect(combinedText).toContain('Container Deployment Guide');
});
// -------------------------------------------------------------------------
// 3. Domain filter chips work
// -------------------------------------------------------------------------
test('domain filter chips are present and clickable', async ({ page }) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
await expect(page.locator('app-entity-card')).toHaveCount(3, { timeout: 10_000 });
// Domain filter chips should be rendered (findings, vex, knowledge)
const filterChips = resultsContainer.locator('[data-role="domain-filter"], .search__filters .search__filter, .search__filter, .chip');
const chipCount = await filterChips.count();
expect(chipCount).toBeGreaterThanOrEqual(1);
// Click the first chip and verify it toggles (gets an active/selected class)
const firstChip = filterChips.first();
await firstChip.click();
// After clicking a filter chip, results should still be visible
// (the component filters client-side or re-fetches)
await expect(resultsContainer).toBeVisible();
});
// -------------------------------------------------------------------------
// 4. Keyboard navigation
// -------------------------------------------------------------------------
test('keyboard navigation: ArrowDown moves selection, Escape closes results', async ({
page,
}) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
const searchInput = await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
// Press ArrowDown to move selection to the first entity card
await searchInput.press('ArrowDown');
// First card should receive a visual active/selected indicator
const firstCard = page.locator('app-entity-card').first();
await expect(firstCard).toBeVisible();
// Press ArrowDown again to move to the second card
await searchInput.press('ArrowDown');
// Press Escape to close the results panel
await page.keyboard.press('Escape');
await expect(resultsContainer).toBeHidden({ timeout: 5_000 });
});
test('keyboard navigation: Enter on selected card triggers primary action', async ({
page,
}) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
const searchInput = await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
await expect(page.locator('app-entity-card')).toHaveCount(3, { timeout: 10_000 });
// Navigate to the first card
await searchInput.press('ArrowDown');
const initialUrl = page.url();
await page.keyboard.press('Enter');
// Enter should trigger the selected card primary route.
await expect.poll(() => page.url(), { timeout: 10_000 }).not.toBe(initialUrl);
});
// -------------------------------------------------------------------------
// 5. Synthesis panel renders
// -------------------------------------------------------------------------
test('synthesis panel renders summary text after search', async ({ page }) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
// Synthesis panel should render
const synthesisPanel = page.locator('app-synthesis-panel');
await expect(synthesisPanel).toBeVisible({ timeout: 10_000 });
// Verify the synthesis summary text is present
const synthesisText = await synthesisPanel.textContent();
expect(synthesisText).toContain('CVE-2024-21626');
expect(synthesisText).toContain('CRITICAL');
// Verify domain coverage indicators
expect(synthesisText).toContain('finding');
expect(synthesisText).toContain('VEX');
});
// -------------------------------------------------------------------------
// 6. Empty state
// -------------------------------------------------------------------------
test('empty state shows "No results" message when API returns zero cards', async ({
page,
}) => {
await mockSearchApi(page, emptySearchResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearchInput(page, 'xyznonexistent999');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
// No entity cards should be rendered
const entityCards = page.locator('app-entity-card');
await expect(entityCards).toHaveCount(0, { timeout: 5_000 });
// A "No results" or empty state message should be visible
const noResultsText = resultsContainer.getByText(/no results/i);
await expect(noResultsText).toBeVisible({ timeout: 5_000 });
});
// -------------------------------------------------------------------------
// 7. Accessibility (axe-core)
// -------------------------------------------------------------------------
test('search results pass axe-core accessibility checks', async ({ page }) => {
await mockSearchApi(page, searchResultsResponse);
await page.goto('/');
await expect(page.locator('aside.sidebar')).toBeVisible({ timeout: 15_000 });
await typeInSearchInput(page, 'CVE-2024-21626');
const resultsContainer = page.locator('.search__results');
await expect(resultsContainer).toBeVisible({ timeout: 10_000 });
// Wait for entity cards to be fully rendered before scanning
await expect(page.locator('app-entity-card')).toHaveCount(3, { timeout: 10_000 });
// Run axe-core against the search results region
const a11yResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.include('.search')
.analyze();
const violations = a11yResults.violations;
// Log violations for debugging but allow the test to surface issues
if (violations.length > 0) {
console.log(
'[a11y] Unified search violations:',
JSON.stringify(
violations.map((v) => ({
id: v.id,
impact: v.impact,
description: v.description,
nodes: v.nodes.length,
})),
null,
2,
),
);
}
// Fail on serious or critical a11y violations
const serious = violations.filter(
(v) => v.impact === 'critical' || v.impact === 'serious',
);
expect(
serious,
`Expected no critical/serious a11y violations but found ${serious.length}: ${serious.map((v) => v.id).join(', ')}`,
).toHaveLength(0);
});
});