Backfill live auth scope and evidence route metadata
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"tokenEndpoint": "https://stella-ops.local/connect/token",
|
"tokenEndpoint": "https://stella-ops.local/connect/token",
|
||||||
"redirectUri": "https://stella-ops.local/auth/callback",
|
"redirectUri": "https://stella-ops.local/auth/callback",
|
||||||
"postLogoutRedirectUri": "https://stella-ops.local/",
|
"postLogoutRedirectUri": "https://stella-ops.local/",
|
||||||
"scope": "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate timeline:read timeline:write",
|
"scope": "openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate registry.admin timeline:read timeline:write",
|
||||||
"audience": "stella-ops-api",
|
"audience": "stella-ops-api",
|
||||||
"dpopAlgorithms": [
|
"dpopAlgorithms": [
|
||||||
"ES256"
|
"ES256"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using System.Threading.Tasks;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using StellaOps.Auth.Abstractions;
|
||||||
|
using StellaOps.Authority.Persistence.InMemory.Stores;
|
||||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||||
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
using StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||||
using StellaOps.Authority.Plugins.Abstractions;
|
using StellaOps.Authority.Plugins.Abstractions;
|
||||||
@@ -21,6 +23,57 @@ namespace StellaOps.Authority.Plugin.Standard.Tests;
|
|||||||
|
|
||||||
public class StandardPluginBootstrapperTests
|
public class StandardPluginBootstrapperTests
|
||||||
{
|
{
|
||||||
|
[Trait("Category", TestCategories.Unit)]
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_EnsuresBootstrapClientsWithoutBootstrapUser()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddOptions<StandardPluginOptions>("standard")
|
||||||
|
.Configure(options =>
|
||||||
|
{
|
||||||
|
options.TenantId = "demo-prod";
|
||||||
|
options.BootstrapClients = new[]
|
||||||
|
{
|
||||||
|
new BootstrapClientOptions
|
||||||
|
{
|
||||||
|
ClientId = "stella-ops-ui",
|
||||||
|
DisplayName = "Stella Ops Console",
|
||||||
|
AllowedGrantTypes = "authorization_code refresh_token",
|
||||||
|
AllowedScopes = $"openid profile {StellaOpsScopes.UiRead} {StellaOpsScopes.RegistryAdmin}",
|
||||||
|
RedirectUris = "https://stella-ops.local/auth/callback https://stella-ops.local/auth/silent-refresh",
|
||||||
|
PostLogoutRedirectUris = "https://stella-ops.local/",
|
||||||
|
RequirePkce = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var clientStore = new InMemoryClientStore();
|
||||||
|
services.AddSingleton<IAuthorityClientStore>(clientStore);
|
||||||
|
services.AddSingleton<IAuthorityRevocationStore>(new StubRevocationStore());
|
||||||
|
services.AddSingleton<TimeProvider>(new FakeTimeProvider(DateTimeOffset.Parse("2025-12-29T13:00:00Z")));
|
||||||
|
services.AddSingleton(sp =>
|
||||||
|
new StandardClientProvisioningStore(
|
||||||
|
"standard",
|
||||||
|
sp.GetRequiredService<IAuthorityClientStore>(),
|
||||||
|
sp.GetRequiredService<IAuthorityRevocationStore>(),
|
||||||
|
sp.GetRequiredService<TimeProvider>()));
|
||||||
|
|
||||||
|
services.AddSingleton<StandardPluginBootstrapper>(sp =>
|
||||||
|
new StandardPluginBootstrapper("standard", sp.GetRequiredService<IServiceScopeFactory>(), NullLogger<StandardPluginBootstrapper>.Instance));
|
||||||
|
|
||||||
|
using var provider = services.BuildServiceProvider();
|
||||||
|
var bootstrapper = provider.GetRequiredService<StandardPluginBootstrapper>();
|
||||||
|
|
||||||
|
await bootstrapper.StartAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var client = await clientStore.FindByClientIdAsync("stella-ops-ui", TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(client);
|
||||||
|
Assert.Contains(StellaOpsScopes.RegistryAdmin, client!.AllowedScopes);
|
||||||
|
Assert.Contains("authorization_code", client.AllowedGrantTypes);
|
||||||
|
Assert.True(client.RequirePkce);
|
||||||
|
Assert.Equal("demo-prod", client.Properties[AuthorityClientMetadataKeys.Tenant]);
|
||||||
|
}
|
||||||
|
|
||||||
[Trait("Category", TestCategories.Unit)]
|
[Trait("Category", TestCategories.Unit)]
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StartAsync_DoesNotThrow_WhenBootstrapFails()
|
public async Task StartAsync_DoesNotThrow_WhenBootstrapFails()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using StellaOps.Auth.Abstractions;
|
||||||
using StellaOps.Authority.Plugin.Standard;
|
using StellaOps.Authority.Plugin.Standard;
|
||||||
using StellaOps.Cryptography;
|
using StellaOps.Cryptography;
|
||||||
|
|
||||||
@@ -25,6 +26,30 @@ public class StandardPluginOptionsTests
|
|||||||
options.Validate("standard");
|
options.Validate("standard");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Trait("Category", TestCategories.Unit)]
|
||||||
|
[Fact]
|
||||||
|
public void Validate_AllowsBootstrapClientWhenConfigured()
|
||||||
|
{
|
||||||
|
var options = new StandardPluginOptions
|
||||||
|
{
|
||||||
|
BootstrapClients = new[]
|
||||||
|
{
|
||||||
|
new BootstrapClientOptions
|
||||||
|
{
|
||||||
|
ClientId = "stella-ops-ui",
|
||||||
|
DisplayName = "Stella Ops Console",
|
||||||
|
AllowedGrantTypes = "authorization_code refresh_token",
|
||||||
|
AllowedScopes = $"openid profile {StellaOpsScopes.UiRead} {StellaOpsScopes.RegistryAdmin}",
|
||||||
|
RedirectUris = "https://stella-ops.local/auth/callback",
|
||||||
|
PostLogoutRedirectUris = "https://stella-ops.local/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Normalize(Path.Combine(Path.GetTempPath(), "config", "standard.yaml"));
|
||||||
|
options.Validate("standard");
|
||||||
|
}
|
||||||
|
|
||||||
[Trait("Category", TestCategories.Unit)]
|
[Trait("Category", TestCategories.Unit)]
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_Throws_WhenBootstrapUserIncomplete()
|
public void Validate_Throws_WhenBootstrapUserIncomplete()
|
||||||
@@ -42,6 +67,30 @@ public class StandardPluginOptionsTests
|
|||||||
Assert.Contains("bootstrapUser", ex.Message, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("bootstrapUser", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Trait("Category", TestCategories.Unit)]
|
||||||
|
[Fact]
|
||||||
|
public void Validate_Throws_WhenConfidentialBootstrapClientMissingSecret()
|
||||||
|
{
|
||||||
|
var options = new StandardPluginOptions
|
||||||
|
{
|
||||||
|
BootstrapClients = new[]
|
||||||
|
{
|
||||||
|
new BootstrapClientOptions
|
||||||
|
{
|
||||||
|
ClientId = "registry-admin",
|
||||||
|
Confidential = true,
|
||||||
|
AllowedGrantTypes = "client_credentials",
|
||||||
|
AllowedScopes = StellaOpsScopes.RegistryAdmin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
options.Normalize(Path.Combine(Path.GetTempPath(), "config", "standard.yaml"));
|
||||||
|
|
||||||
|
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate("standard"));
|
||||||
|
Assert.Contains("clientsecret", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
[Trait("Category", TestCategories.Unit)]
|
[Trait("Category", TestCategories.Unit)]
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_Throws_WhenLockoutWindowMinutesInvalid()
|
public void Validate_Throws_WhenLockoutWindowMinutesInvalid()
|
||||||
@@ -115,6 +164,16 @@ public class StandardPluginOptionsTests
|
|||||||
{
|
{
|
||||||
Username = " admin ",
|
Username = " admin ",
|
||||||
Password = " "
|
Password = " "
|
||||||
|
},
|
||||||
|
BootstrapClients = new[]
|
||||||
|
{
|
||||||
|
new BootstrapClientOptions
|
||||||
|
{
|
||||||
|
ClientId = " Stella-Ops-Ui ",
|
||||||
|
AllowedGrantTypes = " refresh_token authorization_code ",
|
||||||
|
AllowedScopes = $" {StellaOpsScopes.UiRead} {StellaOpsScopes.RegistryAdmin.ToUpperInvariant()} ",
|
||||||
|
RedirectUris = " https://stella-ops.local/auth/callback "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,6 +182,9 @@ public class StandardPluginOptionsTests
|
|||||||
Assert.Equal("tenant-a", options.TenantId);
|
Assert.Equal("tenant-a", options.TenantId);
|
||||||
Assert.Equal("admin", options.BootstrapUser?.Username);
|
Assert.Equal("admin", options.BootstrapUser?.Username);
|
||||||
Assert.Null(options.BootstrapUser?.Password);
|
Assert.Null(options.BootstrapUser?.Password);
|
||||||
|
Assert.Equal("Stella-Ops-Ui", options.BootstrapClients[0].ClientId);
|
||||||
|
Assert.Equal("authorization_code refresh_token", options.BootstrapClients[0].AllowedGrantTypes);
|
||||||
|
Assert.Equal($"{StellaOpsScopes.RegistryAdmin} {StellaOpsScopes.UiRead}", options.BootstrapClients[0].AllowedScopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Trait("Category", TestCategories.Unit)]
|
[Trait("Category", TestCategories.Unit)]
|
||||||
|
|||||||
224
src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs
Normal file
224
src/Web/StellaOps.Web/scripts/live-frontdoor-auth.mjs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const webRoot = path.resolve(__dirname, '..');
|
||||||
|
const outputDirectory = path.join(webRoot, 'output', 'playwright');
|
||||||
|
|
||||||
|
const DEFAULT_BASE_URL = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local';
|
||||||
|
const DEFAULT_USERNAME = process.env.STELLAOPS_FRONTDOOR_USERNAME?.trim() || 'admin';
|
||||||
|
const DEFAULT_PASSWORD = process.env.STELLAOPS_FRONTDOOR_PASSWORD?.trim() || 'Admin@Stella2026!';
|
||||||
|
const DEFAULT_STATE_PATH = path.join(outputDirectory, 'live-frontdoor-auth-state.json');
|
||||||
|
const DEFAULT_REPORT_PATH = path.join(outputDirectory, 'live-frontdoor-auth-report.json');
|
||||||
|
|
||||||
|
function createLocator(page, selectors) {
|
||||||
|
return page.locator(selectors.join(', ')).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickIfVisible(locator, timeoutMs = 5_000) {
|
||||||
|
if (!(await locator.isVisible().catch(() => false))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await locator.click({ timeout: timeoutMs, noWaitAfter: true }).catch(() => {});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillIfVisible(locator, value) {
|
||||||
|
if (!(await locator.isVisible().catch(() => false))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await locator.fill(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForShell(page) {
|
||||||
|
const shellMarkers = [
|
||||||
|
page.locator('app-topbar'),
|
||||||
|
page.locator('aside.sidebar'),
|
||||||
|
page.locator('app-shell'),
|
||||||
|
page.locator('app-root'),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const marker of shellMarkers) {
|
||||||
|
if (await marker.first().isVisible().catch(() => false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.race([
|
||||||
|
...shellMarkers.map((marker) => marker.first().waitFor({ state: 'visible', timeout: 15_000 }).catch(() => {})),
|
||||||
|
page.waitForTimeout(15_000),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authenticateFrontdoor(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl?.trim() || DEFAULT_BASE_URL;
|
||||||
|
const username = options.username?.trim() || DEFAULT_USERNAME;
|
||||||
|
const password = options.password?.trim() || DEFAULT_PASSWORD;
|
||||||
|
const statePath = options.statePath || DEFAULT_STATE_PATH;
|
||||||
|
const reportPath = options.reportPath || DEFAULT_REPORT_PATH;
|
||||||
|
const headless = options.headless ?? true;
|
||||||
|
|
||||||
|
mkdirSync(path.dirname(statePath), { recursive: true });
|
||||||
|
mkdirSync(path.dirname(reportPath), { recursive: true });
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless,
|
||||||
|
args: ['--disable-dev-shm-usage'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
consoleErrors: [],
|
||||||
|
requestFailures: [],
|
||||||
|
responseErrors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') {
|
||||||
|
events.consoleErrors.push(message.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('requestfailed', (request) => {
|
||||||
|
const url = request.url();
|
||||||
|
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.requestFailures.push({
|
||||||
|
method: request.method(),
|
||||||
|
url,
|
||||||
|
error: request.failure()?.errorText ?? 'unknown',
|
||||||
|
page: page.url(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('response', (response) => {
|
||||||
|
const url = response.url();
|
||||||
|
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status() >= 400) {
|
||||||
|
events.responseErrors.push({
|
||||||
|
status: response.status(),
|
||||||
|
method: response.request().method(),
|
||||||
|
url,
|
||||||
|
page: page.url(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/welcome`, {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(1_500);
|
||||||
|
|
||||||
|
const signInTrigger = createLocator(page, [
|
||||||
|
'button:has-text("Sign In")',
|
||||||
|
'button:has-text("Sign in")',
|
||||||
|
'a:has-text("Sign In")',
|
||||||
|
'a:has-text("Sign in")',
|
||||||
|
'button.cta',
|
||||||
|
]);
|
||||||
|
|
||||||
|
await clickIfVisible(signInTrigger);
|
||||||
|
await page.waitForTimeout(1_500);
|
||||||
|
|
||||||
|
const usernameField = createLocator(page, [
|
||||||
|
'input[name="username"]',
|
||||||
|
'input[name="Username"]',
|
||||||
|
'input[type="text"]',
|
||||||
|
'input[type="email"]',
|
||||||
|
]);
|
||||||
|
const passwordField = createLocator(page, [
|
||||||
|
'input[name="password"]',
|
||||||
|
'input[name="Password"]',
|
||||||
|
'input[type="password"]',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hasLoginForm = (await usernameField.count()) > 0 && (await passwordField.count()) > 0;
|
||||||
|
if (page.url().includes('/connect/authorize') || hasLoginForm) {
|
||||||
|
const filledUser = await fillIfVisible(usernameField, username);
|
||||||
|
const filledPassword = await fillIfVisible(passwordField, password);
|
||||||
|
|
||||||
|
if (!filledUser || !filledPassword) {
|
||||||
|
throw new Error(`Authority login form was reached at ${page.url()} but the credentials fields were not interactable.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = createLocator(page, [
|
||||||
|
'button[type="submit"]',
|
||||||
|
'button:has-text("Sign In")',
|
||||||
|
'button:has-text("Sign in")',
|
||||||
|
'button:has-text("Log in")',
|
||||||
|
'button:has-text("Login")',
|
||||||
|
]);
|
||||||
|
|
||||||
|
await submitButton.click({ timeout: 10_000 });
|
||||||
|
|
||||||
|
await page.waitForURL(
|
||||||
|
(url) => !url.toString().includes('/connect/authorize') && !url.toString().includes('/auth/callback'),
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForShell(page);
|
||||||
|
await page.waitForTimeout(2_500);
|
||||||
|
|
||||||
|
await context.storageState({ path: statePath });
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
authenticatedAtUtc: new Date().toISOString(),
|
||||||
|
baseUrl,
|
||||||
|
finalUrl: page.url(),
|
||||||
|
title: await page.title(),
|
||||||
|
cookies: (await context.cookies()).map((cookie) => ({
|
||||||
|
name: cookie.name,
|
||||||
|
domain: cookie.domain,
|
||||||
|
path: cookie.path,
|
||||||
|
secure: cookie.secure,
|
||||||
|
sameSite: cookie.sameSite,
|
||||||
|
})),
|
||||||
|
storage: await page.evaluate(() => ({
|
||||||
|
localStorageEntries: [...Array(localStorage.length)]
|
||||||
|
.map((_, index) => localStorage.key(index))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((key) => [key, localStorage.getItem(key)]),
|
||||||
|
sessionStorageEntries: [...Array(sessionStorage.length)]
|
||||||
|
.map((_, index) => sessionStorage.key(index))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((key) => [key, sessionStorage.getItem(key)]),
|
||||||
|
})),
|
||||||
|
events,
|
||||||
|
statePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const report = await authenticateFrontdoor();
|
||||||
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
||||||
|
main().catch((error) => {
|
||||||
|
process.stderr.write(`[live-frontdoor-auth] ${error instanceof Error ? error.message : String(error)}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { Routes } from '@angular/router';
|
|||||||
* Mounted under /evidence/threads by evidence.routes.ts (Sprint 021).
|
* Mounted under /evidence/threads by evidence.routes.ts (Sprint 021).
|
||||||
* Canonical URLs:
|
* Canonical URLs:
|
||||||
* /evidence/threads - Thread list
|
* /evidence/threads - Thread list
|
||||||
* /evidence/threads/:artifactDigest - Thread detail for a specific artifact
|
* /evidence/threads/:canonicalId - Canonical evidence record detail
|
||||||
*/
|
*/
|
||||||
export const EVIDENCE_THREAD_ROUTES: Routes = [
|
export const EVIDENCE_THREAD_ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ export const EVIDENCE_THREAD_ROUTES: Routes = [
|
|||||||
data: { breadcrumb: 'Threads' },
|
data: { breadcrumb: 'Threads' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':artifactDigest',
|
path: ':canonicalId',
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./components/evidence-thread-view/evidence-thread-view.component').then(
|
import('./components/evidence-thread-view/evidence-thread-view.component').then(
|
||||||
(m) => m.EvidenceThreadViewComponent
|
(m) => m.EvidenceThreadViewComponent
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Routes } from '@angular/router';
|
|||||||
* Canonical URL contract (Sprint 021):
|
* Canonical URL contract (Sprint 021):
|
||||||
* /evidence - Evidence overview
|
* /evidence - Evidence overview
|
||||||
* /evidence/overview - Evidence overview (alias)
|
* /evidence/overview - Evidence overview (alias)
|
||||||
* /evidence/threads - Evidence thread list
|
* /evidence/threads - Evidence thread lookup by PURL
|
||||||
* /evidence/threads/:artifactDigest - Evidence thread detail
|
* /evidence/threads/:canonicalId - Canonical evidence record detail
|
||||||
* /evidence/workspaces/auditor/:artifactDigest - Auditor workspace lens
|
* /evidence/workspaces/auditor/:artifactDigest - Auditor workspace lens
|
||||||
* /evidence/workspaces/developer/:artifactDigest - Developer workspace lens
|
* /evidence/workspaces/developer/:artifactDigest - Developer workspace lens
|
||||||
* /evidence/capsules - Decision capsule list
|
* /evidence/capsules - Decision capsule list
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"redirectUri": "/auth/callback",
|
"redirectUri": "/auth/callback",
|
||||||
"silentRefreshRedirectUri": "/auth/silent-refresh",
|
"silentRefreshRedirectUri": "/auth/silent-refresh",
|
||||||
"postLogoutRedirectUri": "/",
|
"postLogoutRedirectUri": "/",
|
||||||
"scope": "openid profile email offline_access ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit",
|
"scope": "openid profile email offline_access ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit registry.admin",
|
||||||
"audience": "/scanner",
|
"audience": "/scanner",
|
||||||
"dpopAlgorithms": ["ES256"],
|
"dpopAlgorithms": ["ES256"],
|
||||||
"refreshLeewaySeconds": 60
|
"refreshLeewaySeconds": 60
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"logoutEndpoint": "https://authority.example.dev/connect/logout",
|
"logoutEndpoint": "https://authority.example.dev/connect/logout",
|
||||||
"redirectUri": "http://localhost:4400/auth/callback",
|
"redirectUri": "http://localhost:4400/auth/callback",
|
||||||
"postLogoutRedirectUri": "http://localhost:4400/",
|
"postLogoutRedirectUri": "http://localhost:4400/",
|
||||||
"scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit",
|
"scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit registry.admin",
|
||||||
"audience": "https://scanner.example.dev",
|
"audience": "https://scanner.example.dev",
|
||||||
"dpopAlgorithms": ["ES256"],
|
"dpopAlgorithms": ["ES256"],
|
||||||
"refreshLeewaySeconds": 60
|
"refreshLeewaySeconds": 60
|
||||||
|
|||||||
Reference in New Issue
Block a user