Add unit tests for Router configuration and transport layers
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

- Implemented tests for RouterConfig, RoutingOptions, StaticInstanceConfig, and RouterConfigOptions to ensure default values are set correctly.
- Added tests for RouterConfigProvider to validate configurations and ensure defaults are returned when no file is specified.
- Created tests for ConfigValidationResult to check success and error scenarios.
- Developed tests for ServiceCollectionExtensions to verify service registration for RouterConfig.
- Introduced UdpTransportTests to validate serialization, connection, request-response, and error handling in UDP transport.
- Added scripts for signing authority gaps and hashing DevPortal SDK snippets.
This commit is contained in:
StellaOps Bot
2025-12-05 08:01:47 +02:00
parent 635c70e828
commit 6a299d231f
294 changed files with 28434 additions and 1329 deletions

View File

@@ -1,7 +1,7 @@
import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../src/stories/**/*.stories.@(ts|mdx)'],
stories: ['../src/stories/**/*.@(mdx|stories.@(ts))'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',

View File

@@ -111,9 +111,8 @@
"options": {
"configDir": ".storybook",
"browserTarget": "stellaops-web:build",
"port": 4600,
"quiet": true,
"ci": true
"compodoc": false,
"port": 6006
}
},
"build-storybook": {
@@ -121,8 +120,8 @@
"options": {
"configDir": ".storybook",
"browserTarget": "stellaops-web:build",
"outputDir": "storybook-static",
"quiet": true
"compodoc": false,
"outputDir": "storybook-static"
}
}
}

View File

@@ -25,6 +25,7 @@
"@angular/cli": "^17.3.17",
"@angular/compiler-cli": "^17.3.0",
"@axe-core/playwright": "4.8.4",
"@chromatic-com/storybook": "^1.9.0",
"@playwright/test": "^1.47.2",
"@storybook/addon-a11y": "8.1.0",
"@storybook/addon-essentials": "8.1.0",
@@ -1195,6 +1196,7 @@
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.17.tgz",
"integrity": "sha512-FgOvf9q5d23Cpa7cjP1FYti/v8S1FTm8DEkW3TY8lkkoxh3isu28GFKcLD1p/XF3yqfPkPVHToOFla5QwsEgBQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@angular-devkit/architect": "0.1703.17",
@@ -3260,6 +3262,76 @@
"node": ">=6.9.0"
}
},
"node_modules/@chromatic-com/storybook": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-1.9.0.tgz",
"integrity": "sha512-vYQ+TcfktEE3GHnLZXHCzXF/sN9dw+KivH8a5cmPyd9YtQs7fZtHrEgsIjWpYycXiweKMo1Lm1RZsjxk8DH3rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chromatic": "^11.4.0",
"filesize": "^10.0.12",
"jsonfile": "^6.1.0",
"react-confetti": "^6.1.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.22.18"
}
},
"node_modules/@chromatic-com/storybook/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@chromatic-com/storybook/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@chromatic-com/storybook/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@chromatic-com/storybook/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"dev": true,
@@ -8538,6 +8610,30 @@
"node": ">=10"
}
},
"node_modules/chromatic": {
"version": "11.29.0",
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.29.0.tgz",
"integrity": "sha512-yisBlntp9hHVj19lIQdpTlcYIXuU9H/DbFuu6tyWHmj6hWT2EtukCCcxYXL78XdQt1vm2GfIrtgtKpj/Rzmo4A==",
"dev": true,
"license": "MIT",
"bin": {
"chroma": "dist/bin.js",
"chromatic": "dist/bin.js",
"chromatic-cli": "dist/bin.js"
},
"peerDependencies": {
"@chromatic-com/cypress": "^0.*.* || ^1.0.0",
"@chromatic-com/playwright": "^0.*.* || ^1.0.0"
},
"peerDependenciesMeta": {
"@chromatic-com/cypress": {
"optional": true
},
"@chromatic-com/playwright": {
"optional": true
}
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"dev": true,
@@ -10835,6 +10931,16 @@
"node": ">=10"
}
},
"node_modules/filesize": {
"version": "10.1.6",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
"integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 10.4.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"dev": true,
@@ -15327,6 +15433,22 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-confetti": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz",
"integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"tween-functions": "^1.2.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -17673,6 +17795,13 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==",
"dev": true,
"license": "BSD"
},
"node_modules/type-detect": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",

View File

@@ -40,6 +40,7 @@
"@angular/cli": "^17.3.17",
"@angular/compiler-cli": "^17.3.0",
"@axe-core/playwright": "4.8.4",
"@chromatic-com/storybook": "^1.9.0",
"@playwright/test": "^1.47.2",
"@storybook/addon-a11y": "8.1.0",
"@storybook/addon-essentials": "8.1.0",

View File

@@ -127,3 +127,68 @@ export const requireOrchQuotaGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.ORCH_READ, StellaOpsScopes.ORCH_QUOTA],
'/console/profile'
);
// Pre-built guards for Policy Studio scope requirements (UI-POLICY-20-003)
/**
* Guard requiring policy:read scope for Policy Studio viewer access.
* Redirects to /console/profile if user lacks Policy viewer access.
*/
export const requirePolicyViewerGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ],
'/console/profile'
);
/**
* Guard requiring policy:author and policy:edit scopes for policy authoring.
* Allows creating and editing policy drafts.
*/
export const requirePolicyAuthorGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_AUTHOR, StellaOpsScopes.POLICY_EDIT],
'/console/profile'
);
/**
* Guard requiring policy:review scope for policy review workflow.
* Allows reviewing policy drafts before approval.
*/
export const requirePolicyReviewerGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_REVIEW],
'/console/profile'
);
/**
* Guard requiring policy:approve scope for policy approval workflow.
* Allows approving or rejecting policy drafts.
*/
export const requirePolicyApproverGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_REVIEW, StellaOpsScopes.POLICY_APPROVE],
'/console/profile'
);
/**
* Guard requiring policy:operate and policy:activate scopes for policy operations.
* Allows activating and running policies in environments.
*/
export const requirePolicyOperatorGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_OPERATE, StellaOpsScopes.POLICY_ACTIVATE],
'/console/profile'
);
/**
* Guard requiring policy:simulate scope for policy simulation.
* Allows running what-if simulations against policies.
*/
export const requirePolicySimulatorGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_SIMULATE],
'/console/profile'
);
/**
* Guard requiring policy:audit scope for policy audit trails.
* Allows viewing policy change history and audit logs.
*/
export const requirePolicyAuditGuard: CanMatchFn = requireScopesGuard(
[StellaOpsScopes.POLICY_READ, StellaOpsScopes.POLICY_AUDIT],
'/console/profile'
);

View File

@@ -46,6 +46,17 @@ export interface AuthService {
canOperateOrchestrator(): boolean;
canManageOrchestratorQuotas(): boolean;
canInitiateBackfill(): boolean;
// Policy Studio access (UI-POLICY-20-003)
canViewPolicies(): boolean;
canAuthorPolicies(): boolean;
canEditPolicies(): boolean;
canReviewPolicies(): boolean;
canApprovePolicies(): boolean;
canOperatePolicies(): boolean;
canActivatePolicies(): boolean;
canSimulatePolicies(): boolean;
canPublishPolicies(): boolean;
canAuditPolicies(): boolean;
}
// ============================================================================
@@ -67,10 +78,19 @@ const MOCK_USER: AuthUser = {
StellaOpsScopes.GRAPH_EXPORT,
// SBOM permissions
StellaOpsScopes.SBOM_READ,
// Policy permissions
// Policy permissions (Policy Studio - UI-POLICY-20-003)
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_EVALUATE,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.POLICY_AUTHOR,
StellaOpsScopes.POLICY_EDIT,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_SUBMIT,
StellaOpsScopes.POLICY_APPROVE,
StellaOpsScopes.POLICY_OPERATE,
StellaOpsScopes.POLICY_ACTIVATE,
StellaOpsScopes.POLICY_RUN,
StellaOpsScopes.POLICY_AUDIT,
// Scanner permissions
StellaOpsScopes.SCANNER_READ,
// Exception permissions
@@ -144,6 +164,47 @@ export class MockAuthService implements AuthService {
canInitiateBackfill(): boolean {
return this.hasScope(StellaOpsScopes.ORCH_BACKFILL);
}
// Policy Studio access methods (UI-POLICY-20-003)
canViewPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_READ);
}
canAuthorPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_AUTHOR);
}
canEditPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_EDIT);
}
canReviewPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_REVIEW);
}
canApprovePolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_APPROVE);
}
canOperatePolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_OPERATE);
}
canActivatePolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_ACTIVATE);
}
canSimulatePolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_SIMULATE);
}
canPublishPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_PUBLISH);
}
canAuditPolicies(): boolean {
return this.hasScope(StellaOpsScopes.POLICY_AUDIT);
}
}
// Re-export scopes for convenience

View File

@@ -28,11 +28,24 @@ export const StellaOpsScopes = {
SCANNER_WRITE: 'scanner:write',
SCANNER_SCAN: 'scanner:scan',
// Policy scopes
// Policy scopes (full Policy Studio workflow - UI-POLICY-20-003)
POLICY_READ: 'policy:read',
POLICY_WRITE: 'policy:write',
POLICY_EVALUATE: 'policy:evaluate',
POLICY_SIMULATE: 'policy:simulate',
// Policy Studio authoring & review workflow
POLICY_AUTHOR: 'policy:author',
POLICY_EDIT: 'policy:edit',
POLICY_REVIEW: 'policy:review',
POLICY_SUBMIT: 'policy:submit',
POLICY_APPROVE: 'policy:approve',
// Policy operations & execution
POLICY_OPERATE: 'policy:operate',
POLICY_ACTIVATE: 'policy:activate',
POLICY_RUN: 'policy:run',
POLICY_PUBLISH: 'policy:publish', // Requires interactive auth
POLICY_PROMOTE: 'policy:promote', // Requires interactive auth
POLICY_AUDIT: 'policy:audit',
// Exception scopes
EXCEPTION_READ: 'exception:read',
@@ -128,6 +141,64 @@ export const ScopeGroups = {
StellaOpsScopes.ORCH_BACKFILL,
StellaOpsScopes.UI_READ,
] as const,
// Policy Studio scope groups (UI-POLICY-20-003)
POLICY_VIEWER: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.UI_READ,
] as const,
POLICY_AUTHOR: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_AUTHOR,
StellaOpsScopes.POLICY_EDIT,
StellaOpsScopes.POLICY_WRITE,
StellaOpsScopes.POLICY_SUBMIT,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.UI_READ,
] as const,
POLICY_REVIEWER: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.UI_READ,
] as const,
POLICY_APPROVER: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_APPROVE,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.UI_READ,
] as const,
POLICY_OPERATOR: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_OPERATE,
StellaOpsScopes.POLICY_ACTIVATE,
StellaOpsScopes.POLICY_RUN,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.UI_READ,
] as const,
POLICY_ADMIN: [
StellaOpsScopes.POLICY_READ,
StellaOpsScopes.POLICY_AUTHOR,
StellaOpsScopes.POLICY_EDIT,
StellaOpsScopes.POLICY_WRITE,
StellaOpsScopes.POLICY_REVIEW,
StellaOpsScopes.POLICY_SUBMIT,
StellaOpsScopes.POLICY_APPROVE,
StellaOpsScopes.POLICY_OPERATE,
StellaOpsScopes.POLICY_ACTIVATE,
StellaOpsScopes.POLICY_RUN,
StellaOpsScopes.POLICY_PUBLISH,
StellaOpsScopes.POLICY_PROMOTE,
StellaOpsScopes.POLICY_AUDIT,
StellaOpsScopes.POLICY_SIMULATE,
StellaOpsScopes.UI_READ,
] as const,
} as const;
/**
@@ -149,6 +220,18 @@ export const ScopeLabels: Record<StellaOpsScope, string> = {
'policy:write': 'Edit Policies',
'policy:evaluate': 'Evaluate Policies',
'policy:simulate': 'Simulate Policy Changes',
// Policy Studio workflow scopes (UI-POLICY-20-003)
'policy:author': 'Author Policy Drafts',
'policy:edit': 'Edit Policy Configuration',
'policy:review': 'Review Policy Drafts',
'policy:submit': 'Submit Policies for Review',
'policy:approve': 'Approve/Reject Policies',
'policy:operate': 'Operate Policy Promotions',
'policy:activate': 'Activate Policies',
'policy:run': 'Trigger Policy Runs',
'policy:publish': 'Publish Policy Versions',
'policy:promote': 'Promote Between Environments',
'policy:audit': 'Audit Policy Activity',
'exception:read': 'View Exceptions',
'exception:write': 'Create Exceptions',
'exception:approve': 'Approve Exceptions',

View File

@@ -0,0 +1,16 @@
/**
* Policy Studio editor module exports.
*
* @task UI-POLICY-20-001
*/
export {
STELLA_DSL_LANGUAGE_ID,
stellaDslMonarchLanguage,
stellaDslLanguageConfiguration,
stellaDslThemeRules,
registerStellaDslLanguage,
defineStellaDslTheme,
} from './stella-dsl.language';
export { registerStellaDslCompletions } from './stella-dsl.completions';

View File

@@ -0,0 +1,451 @@
/**
* Monaco completion provider for Stella Policy DSL.
*
* Provides IntelliSense suggestions for:
* - Keywords and syntax structures
* - Built-in functions
* - Namespace fields
* - VEX statuses and justifications
*
* @task UI-POLICY-20-001
*/
import type * as Monaco from 'monaco-editor';
import { STELLA_DSL_LANGUAGE_ID } from './stella-dsl.language';
/**
* Completion items for stella-dsl keywords.
*/
const keywordCompletions: Monaco.languages.CompletionItem[] = [
{
label: 'policy',
kind: 14, // Keyword
insertText: 'policy "${1:PolicyName}" syntax "stella-dsl@1" {\n\t$0\n}',
insertTextRules: 4, // InsertAsSnippet
documentation: 'Define a new policy document.',
detail: 'Policy Declaration',
},
{
label: 'metadata',
kind: 14,
insertText: 'metadata {\n\tdescription = "${1:description}"\n\ttags = [$2]\n}',
insertTextRules: 4,
documentation: 'Define metadata for the policy.',
detail: 'Metadata Section',
},
{
label: 'profile',
kind: 14,
insertText: 'profile ${1:severity} {\n\t$0\n}',
insertTextRules: 4,
documentation: 'Define a profile block for scoring modifiers.',
detail: 'Profile Section',
},
{
label: 'settings',
kind: 14,
insertText: 'settings {\n\t${1:shadow} = ${2:true};\n}',
insertTextRules: 4,
documentation: 'Configure evaluation settings.',
detail: 'Settings Section',
},
{
label: 'rule',
kind: 14,
insertText: 'rule ${1:rule_name} priority ${2:10} {\n\twhen ${3:condition}\n\tthen ${4:action}\n\tbecause "${5:rationale}";\n}',
insertTextRules: 4,
documentation: 'Define a policy rule with when/then logic.',
detail: 'Rule Definition',
},
{
label: 'map',
kind: 14,
insertText: 'map ${1:name} {\n\tsource "${2:source}" => ${3:0.0};\n}',
insertTextRules: 4,
documentation: 'Define a scoring map within a profile.',
detail: 'Profile Map',
},
{
label: 'env',
kind: 14,
insertText: 'env ${1:name} {\n\tif ${2:condition} then ${3:value};\n}',
insertTextRules: 4,
documentation: 'Define environment-based adjustments.',
detail: 'Environment Map',
},
{
label: 'when',
kind: 14,
insertText: 'when ${1:condition}',
insertTextRules: 4,
documentation: 'Condition clause for rule execution.',
detail: 'Rule Condition',
},
{
label: 'then',
kind: 14,
insertText: 'then ${1:action}',
insertTextRules: 4,
documentation: 'Action clause executed when condition is true.',
detail: 'Rule Action',
},
{
label: 'else',
kind: 14,
insertText: 'else ${1:action}',
insertTextRules: 4,
documentation: 'Fallback action clause.',
detail: 'Rule Else Action',
},
{
label: 'because',
kind: 14,
insertText: 'because "${1:rationale}"',
insertTextRules: 4,
documentation: 'Mandatory rationale for status/severity changes.',
detail: 'Rule Rationale',
},
];
/**
* Completion items for built-in functions.
*/
const functionCompletions: Monaco.languages.CompletionItem[] = [
{
label: 'normalize_cvss',
kind: 1, // Function
insertText: 'normalize_cvss(${1:advisory})',
insertTextRules: 4,
documentation: 'Parse advisory for CVSS data and return severity scalar.',
detail: 'Advisory → SeverityScalar',
},
{
label: 'cvss',
kind: 1,
insertText: 'cvss(${1:score}, "${2:vector}")',
insertTextRules: 4,
documentation: 'Construct a severity object from score and vector.',
detail: 'double × string → SeverityScalar',
},
{
label: 'severity_band',
kind: 1,
insertText: 'severity_band("${1|critical,high,medium,low,none|}")',
insertTextRules: 4,
documentation: 'Normalise severity string to band.',
detail: 'string → SeverityBand',
},
{
label: 'risk_score',
kind: 1,
insertText: 'risk_score(${1:base}, ${2:modifier})',
insertTextRules: 4,
documentation: 'Calculate risk by multiplying severity × trust × reachability.',
detail: 'Variadic',
},
{
label: 'reach_state',
kind: 1,
insertText: 'reach_state("${1|reachable,unreachable,unknown|}")',
insertTextRules: 4,
documentation: 'Normalise reachability state string.',
detail: 'string → ReachState',
},
{
label: 'exists',
kind: 1,
insertText: 'exists(${1:expression})',
insertTextRules: 4,
documentation: 'Return true when value is non-null/empty.',
detail: '→ bool',
},
{
label: 'coalesce',
kind: 1,
insertText: 'coalesce(${1:a}, ${2:b})',
insertTextRules: 4,
documentation: 'Return first non-null argument.',
detail: '→ value',
},
{
label: 'days_between',
kind: 1,
insertText: 'days_between(${1:dateA}, ${2:dateB})',
insertTextRules: 4,
documentation: 'Calculate absolute day difference (UTC).',
detail: '→ int',
},
{
label: 'percent_of',
kind: 1,
insertText: 'percent_of(${1:part}, ${2:whole})',
insertTextRules: 4,
documentation: 'Calculate percentage for scoring adjustments.',
detail: '→ double',
},
{
label: 'lowercase',
kind: 1,
insertText: 'lowercase(${1:text})',
insertTextRules: 4,
documentation: 'Normalise string casing (InvariantCulture).',
detail: 'string → string',
},
];
/**
* Completion items for VEX functions.
*/
const vexFunctionCompletions: Monaco.languages.CompletionItem[] = [
{
label: 'vex.any',
kind: 1,
insertText: 'vex.any(${1:status} ${2|==,!=,in|} ${3:value})',
insertTextRules: 4,
documentation: 'True if any VEX statement satisfies the predicate.',
detail: '(Statement → bool) → bool',
},
{
label: 'vex.all',
kind: 1,
insertText: 'vex.all(${1:status} ${2|==,!=,in|} ${3:value})',
insertTextRules: 4,
documentation: 'True if all VEX statements satisfy the predicate.',
detail: '(Statement → bool) → bool',
},
{
label: 'vex.latest',
kind: 1,
insertText: 'vex.latest()',
insertTextRules: 4,
documentation: 'Return the lexicographically newest VEX statement.',
detail: '→ Statement',
},
{
label: 'vex.count',
kind: 1,
insertText: 'vex.count(${1:predicate})',
insertTextRules: 4,
documentation: 'Count VEX statements matching predicate.',
detail: '→ int',
},
];
/**
* Completion items for namespace fields.
*/
const namespaceCompletions: Monaco.languages.CompletionItem[] = [
// SBOM fields
{ label: 'sbom.purl', kind: 5, insertText: 'sbom.purl', documentation: 'Package URL of the component.' },
{ label: 'sbom.name', kind: 5, insertText: 'sbom.name', documentation: 'Component name.' },
{ label: 'sbom.version', kind: 5, insertText: 'sbom.version', documentation: 'Component version.' },
{ label: 'sbom.licenses', kind: 5, insertText: 'sbom.licenses', documentation: 'Component licenses.' },
{ label: 'sbom.layerDigest', kind: 5, insertText: 'sbom.layerDigest', documentation: 'Container layer digest.' },
{ label: 'sbom.tags', kind: 5, insertText: 'sbom.tags', documentation: 'Component tags.' },
{ label: 'sbom.usedByEntrypoint', kind: 5, insertText: 'sbom.usedByEntrypoint', documentation: 'Whether component is used by entrypoint.' },
{ label: 'sbom.has_tag', kind: 1, insertText: 'sbom.has_tag("${1:tag}")', insertTextRules: 4, documentation: 'Check SBOM inventory tag.' },
{ label: 'sbom.any_component', kind: 1, insertText: 'sbom.any_component(${1:predicate})', insertTextRules: 4, documentation: 'Iterate SBOM components.' },
// Advisory fields
{ label: 'advisory.id', kind: 5, insertText: 'advisory.id', documentation: 'Advisory identifier.' },
{ label: 'advisory.source', kind: 5, insertText: 'advisory.source', documentation: 'Advisory source (GHSA, OSV, etc.).' },
{ label: 'advisory.aliases', kind: 5, insertText: 'advisory.aliases', documentation: 'Advisory aliases (CVE, etc.).' },
{ label: 'advisory.severity', kind: 5, insertText: 'advisory.severity', documentation: 'Advisory severity.' },
{ label: 'advisory.cvss', kind: 5, insertText: 'advisory.cvss', documentation: 'CVSS score.' },
{ label: 'advisory.publishedAt', kind: 5, insertText: 'advisory.publishedAt', documentation: 'Publication date.' },
{ label: 'advisory.modifiedAt', kind: 5, insertText: 'advisory.modifiedAt', documentation: 'Last modification date.' },
{ label: 'advisory.has_tag', kind: 1, insertText: 'advisory.has_tag("${1:tag}")', insertTextRules: 4, documentation: 'Check advisory metadata tag.' },
{ label: 'advisory.matches', kind: 1, insertText: 'advisory.matches("${1:pattern}")', insertTextRules: 4, documentation: 'Glob match against advisory identifiers.' },
// VEX fields
{ label: 'vex.status', kind: 5, insertText: 'vex.status', documentation: 'VEX status.' },
{ label: 'vex.justification', kind: 5, insertText: 'vex.justification', documentation: 'VEX justification.' },
{ label: 'vex.statementId', kind: 5, insertText: 'vex.statementId', documentation: 'VEX statement ID.' },
{ label: 'vex.timestamp', kind: 5, insertText: 'vex.timestamp', documentation: 'VEX timestamp.' },
{ label: 'vex.scope', kind: 5, insertText: 'vex.scope', documentation: 'VEX scope.' },
// Signals fields
{ label: 'signals.trust_score', kind: 5, insertText: 'signals.trust_score', documentation: 'Trust score (01).' },
{ label: 'signals.reachability.state', kind: 5, insertText: 'signals.reachability.state', documentation: 'Reachability state.' },
{ label: 'signals.reachability.score', kind: 5, insertText: 'signals.reachability.score', documentation: 'Reachability score (01).' },
{ label: 'signals.entropy_penalty', kind: 5, insertText: 'signals.entropy_penalty', documentation: 'Entropy penalty (00.3).' },
{ label: 'signals.uncertainty.level', kind: 5, insertText: 'signals.uncertainty.level', documentation: 'Uncertainty level (U1U3).' },
{ label: 'signals.runtime_hits', kind: 5, insertText: 'signals.runtime_hits', documentation: 'Runtime hit indicator.' },
// Telemetry fields
{ label: 'telemetry.reachability.state', kind: 5, insertText: 'telemetry.reachability.state', documentation: 'Telemetry reachability state.' },
{ label: 'telemetry.reachability.score', kind: 5, insertText: 'telemetry.reachability.score', documentation: 'Telemetry reachability score.' },
// Run fields
{ label: 'run.policyId', kind: 5, insertText: 'run.policyId', documentation: 'Policy ID.' },
{ label: 'run.policyVersion', kind: 5, insertText: 'run.policyVersion', documentation: 'Policy version.' },
{ label: 'run.tenant', kind: 5, insertText: 'run.tenant', documentation: 'Tenant ID.' },
{ label: 'run.timestamp', kind: 5, insertText: 'run.timestamp', documentation: 'Run timestamp.' },
// Secret fields
{ label: 'secret.hasFinding', kind: 1, insertText: 'secret.hasFinding(${1:ruleId})', insertTextRules: 4, documentation: 'Check for secret leak findings.' },
{ label: 'secret.match.count', kind: 1, insertText: 'secret.match.count(${1:ruleId})', insertTextRules: 4, documentation: 'Count secret findings.' },
{ label: 'secret.bundle.version', kind: 1, insertText: 'secret.bundle.version("${1:version}")', insertTextRules: 4, documentation: 'Check secret rule bundle version.' },
{ label: 'secret.mask.applied', kind: 5, insertText: 'secret.mask.applied', documentation: 'Whether masking succeeded.' },
];
/**
* Completion items for action keywords.
*/
const actionCompletions: Monaco.languages.CompletionItem[] = [
{
label: 'status :=',
kind: 14,
insertText: 'status := "${1|affected,not_affected,fixed,suppressed,under_investigation,escalated|}"',
insertTextRules: 4,
documentation: 'Set the finding status.',
detail: 'Status Assignment',
},
{
label: 'severity :=',
kind: 14,
insertText: 'severity := ${1:expression}',
insertTextRules: 4,
documentation: 'Set the finding severity.',
detail: 'Severity Assignment',
},
{
label: 'ignore',
kind: 14,
insertText: 'ignore until ${1:date} because "${2:rationale}"',
insertTextRules: 4,
documentation: 'Temporarily suppress finding until date.',
detail: 'Ignore Action',
},
{
label: 'escalate',
kind: 14,
insertText: 'escalate to severity_band("${1|critical,high|}") when ${2:condition}',
insertTextRules: 4,
documentation: 'Escalate severity when condition is true.',
detail: 'Escalate Action',
},
{
label: 'warn',
kind: 14,
insertText: 'warn message "${1:text}"',
insertTextRules: 4,
documentation: 'Add warning verdict.',
detail: 'Warn Action',
},
{
label: 'defer',
kind: 14,
insertText: 'defer until ${1:condition}',
insertTextRules: 4,
documentation: 'Defer finding evaluation.',
detail: 'Defer Action',
},
{
label: 'annotate',
kind: 14,
insertText: 'annotate ${1:key} := ${2:value}',
insertTextRules: 4,
documentation: 'Add free-form annotation to explain payload.',
detail: 'Annotate Action',
},
{
label: 'requireVex',
kind: 14,
insertText: 'requireVex {\n\tvendors = [${1:"Vendor"}]\n\tjustifications = [${2:"component_not_present"}]\n}',
insertTextRules: 4,
documentation: 'Require matching VEX evidence.',
detail: 'Require VEX Action',
},
];
/**
* Completion items for VEX statuses.
*/
const vexStatusCompletions: Monaco.languages.CompletionItem[] = [
{ label: 'affected', kind: 21, insertText: '"affected"', documentation: 'Component is affected by the vulnerability.' },
{ label: 'not_affected', kind: 21, insertText: '"not_affected"', documentation: 'Component is not affected.' },
{ label: 'fixed', kind: 21, insertText: '"fixed"', documentation: 'Vulnerability has been fixed.' },
{ label: 'suppressed', kind: 21, insertText: '"suppressed"', documentation: 'Finding is suppressed.' },
{ label: 'under_investigation', kind: 21, insertText: '"under_investigation"', documentation: 'Under investigation.' },
{ label: 'escalated', kind: 21, insertText: '"escalated"', documentation: 'Finding has been escalated.' },
];
/**
* Completion items for VEX justifications.
*/
const vexJustificationCompletions: Monaco.languages.CompletionItem[] = [
{ label: 'component_not_present', kind: 21, insertText: '"component_not_present"', documentation: 'Component is not present in the product.' },
{ label: 'vulnerable_code_not_present', kind: 21, insertText: '"vulnerable_code_not_present"', documentation: 'Vulnerable code is not present.' },
{ label: 'vulnerable_code_not_in_execute_path', kind: 21, insertText: '"vulnerable_code_not_in_execute_path"', documentation: 'Vulnerable code is not in execution path.' },
{ label: 'vulnerable_code_cannot_be_controlled_by_adversary', kind: 21, insertText: '"vulnerable_code_cannot_be_controlled_by_adversary"', documentation: 'Vulnerable code cannot be controlled by adversary.' },
{ label: 'inline_mitigations_already_exist', kind: 21, insertText: '"inline_mitigations_already_exist"', documentation: 'Inline mitigations already exist.' },
];
/**
* Registers the completion provider for stella-dsl.
*
* @param monaco - Monaco editor namespace
*/
export function registerStellaDslCompletions(monaco: typeof Monaco): Monaco.IDisposable {
return monaco.languages.registerCompletionItemProvider(STELLA_DSL_LANGUAGE_ID, {
triggerCharacters: ['.', '"', '(', ' '],
provideCompletionItems(
model: Monaco.editor.ITextModel,
position: Monaco.Position,
_context: Monaco.languages.CompletionContext,
_token: Monaco.CancellationToken
): Monaco.languages.ProviderResult<Monaco.languages.CompletionList> {
const word = model.getWordUntilPosition(position);
const range: Monaco.IRange = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
const lineContent = model.getLineContent(position.lineNumber);
const textUntilPosition = lineContent.substring(0, position.column - 1);
// Determine context and provide relevant completions
const suggestions: Monaco.languages.CompletionItem[] = [];
// Check for namespace prefix
if (textUntilPosition.endsWith('sbom.') || textUntilPosition.endsWith('advisory.') ||
textUntilPosition.endsWith('vex.') || textUntilPosition.endsWith('signals.') ||
textUntilPosition.endsWith('telemetry.') || textUntilPosition.endsWith('run.') ||
textUntilPosition.endsWith('secret.') || textUntilPosition.endsWith('env.')) {
suggestions.push(...namespaceCompletions.map(c => ({ ...c, range })));
}
// Check for VEX status context
if (textUntilPosition.match(/status\s*(==|!=|:=|in)\s*["[]?$/)) {
suggestions.push(...vexStatusCompletions.map(c => ({ ...c, range })));
}
// Check for VEX justification context
if (textUntilPosition.match(/justification\s*(==|!=|in)\s*["[]?$/)) {
suggestions.push(...vexJustificationCompletions.map(c => ({ ...c, range })));
}
// Check for action context (after 'then' or 'else')
if (textUntilPosition.match(/\b(then|else)\s*$/)) {
suggestions.push(...actionCompletions.map(c => ({ ...c, range })));
}
// Default: provide all completions
if (suggestions.length === 0) {
suggestions.push(
...keywordCompletions.map(c => ({ ...c, range })),
...functionCompletions.map(c => ({ ...c, range })),
...vexFunctionCompletions.map(c => ({ ...c, range })),
...namespaceCompletions.map(c => ({ ...c, range })),
...actionCompletions.map(c => ({ ...c, range }))
);
}
return { suggestions };
},
});
}

View File

@@ -0,0 +1,367 @@
/**
* Monaco Editor language definition for Stella Policy DSL (`stella-dsl@1`).
*
* This provides syntax highlighting, bracket matching, and folding support
* for the Stella policy language used by the Policy Engine.
*
* @see docs/policy/dsl.md for grammar specification
* @task UI-POLICY-20-001
*/
import type * as Monaco from 'monaco-editor';
export const STELLA_DSL_LANGUAGE_ID = 'stella-dsl';
/**
* Monarch tokenizer configuration for stella-dsl.
* Provides syntax highlighting based on the DSL grammar.
*/
export const stellaDslMonarchLanguage: Monaco.languages.IMonarchLanguage = {
defaultToken: 'invalid',
tokenPostfix: '.stella',
// DSL keywords from grammar
keywords: [
'policy',
'syntax',
'metadata',
'profile',
'settings',
'rule',
'helper',
'map',
'env',
'when',
'then',
'else',
'because',
'priority',
'and',
'or',
'not',
'in',
'source',
],
// Action keywords
actionKeywords: [
'ignore',
'escalate',
'require',
'requireVex',
'warn',
'defer',
'annotate',
'until',
'to',
'message',
],
// Built-in functions
builtinFunctions: [
'normalize_cvss',
'cvss',
'severity_band',
'risk_score',
'reach_state',
'exists',
'coalesce',
'days_between',
'percent_of',
'lowercase',
],
// Namespace identifiers
namespaces: [
'sbom',
'advisory',
'vex',
'run',
'env',
'telemetry',
'signals',
'secret',
'profile',
],
// VEX-related constants
vexStatuses: [
'affected',
'not_affected',
'fixed',
'suppressed',
'under_investigation',
'escalated',
],
vexJustifications: [
'component_not_present',
'vulnerable_code_not_present',
'vulnerable_code_not_in_execute_path',
'vulnerable_code_cannot_be_controlled_by_adversary',
'inline_mitigations_already_exist',
],
// Severity levels
severityLevels: ['critical', 'high', 'medium', 'low', 'none', 'unknown'],
// Reachability states
reachabilityStates: ['reachable', 'unreachable', 'unknown'],
// Operators
operators: [
'=',
':=',
'=>',
'==',
'!=',
'<',
'<=',
'>',
'>=',
],
// Symbol patterns
symbols: /[=><!~?:&|+\-*\/\^%]+/,
// Escape sequences
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
// Tokenizer rules
tokenizer: {
root: [
// Whitespace
{ include: '@whitespace' },
// Comments
[/\/\/.*$/, 'comment'],
[/\/\*/, 'comment', '@comment'],
// Strings
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/"/, 'string', '@string'],
// Numbers
[/\d+%/, 'number.percentage'],
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
[/\d+/, 'number'],
// Booleans
[/\b(true|false)\b/, 'constant.language.boolean'],
// Policy declaration
[/\b(policy)\b/, 'keyword.declaration'],
[/\b(syntax)\b/, 'keyword.declaration'],
// Section keywords
[/\b(metadata|profile|settings|rule|helper)\b/, 'keyword.section'],
// Conditional keywords
[/\b(when|then|else|because)\b/, 'keyword.control'],
// Logical operators
[/\b(and|or|not|in)\b/, 'keyword.operator'],
// Action keywords
[
/\b(ignore|escalate|requireVex|require|warn|defer|annotate|until|to|message)\b/,
'keyword.action',
],
// Priority keyword
[/\b(priority)\b/, 'keyword.modifier'],
// Map/env keywords
[/\b(map|env|source)\b/, 'keyword.declaration'],
// Built-in functions
[
/\b(normalize_cvss|cvss|severity_band|risk_score|reach_state|exists|coalesce|days_between|percent_of|lowercase)\b/,
'support.function',
],
// VEX helper functions
[/\b(vex)\.(any|all|latest|count)\b/, 'support.function.vex'],
// Advisory helper functions
[/\b(advisory)\.(has_tag|matches)\b/, 'support.function.advisory'],
// SBOM helper functions
[/\b(sbom)\.(has_tag|any_component)\b/, 'support.function.sbom'],
// Secret helper functions
[
/\b(secret)\.(hasFinding|match\.count|bundle\.version|mask\.applied|path\.allowlist)\b/,
'support.function.secret',
],
// Ruby scope helpers
[
/\b(ruby)\.(group|groups|declared_only|source|capability|capability_any)\b/,
'support.function.ruby',
],
// Namespace identifiers
[
/\b(sbom|advisory|vex|run|env|telemetry|signals|secret|profile)\b/,
'variable.namespace',
],
// VEX status constants
[
/\b(affected|not_affected|fixed|suppressed|under_investigation|escalated)\b/,
'constant.language.vex-status',
],
// Severity levels
[
/\b(critical|high|medium|low|none)\b(?=\s*["\)])/,
'constant.language.severity',
],
// Reachability states
[
/\b(reachable|unreachable|unknown)\b(?=\s*["\)])/,
'constant.language.reachability',
],
// Delimiters and operators
[/[{}()\[\]]/, '@brackets'],
[/[;,.]/, 'delimiter'],
[/=>/, 'operator.arrow'],
[/:=/, 'operator.assignment'],
[/[=><!~?:&|+\-*\/\^%]+/, 'operator'],
// Identifiers
[/[a-zA-Z_]\w*/, 'identifier'],
],
whitespace: [[/\s+/, 'white']],
comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment'],
],
string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop'],
],
},
};
/**
* Language configuration for bracket matching, auto-closing, and folding.
*/
export const stellaDslLanguageConfiguration: Monaco.languages.LanguageConfiguration =
{
comments: {
lineComment: '//',
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"', notIn: ['string'] },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
],
folding: {
markers: {
start: /^\s*(policy|rule|profile|metadata|settings|map|env)\b/,
end: /^\s*\}/,
},
},
indentationRules: {
increaseIndentPattern: /^\s*(policy|rule|profile|metadata|settings|map|env|when|then|else)\b.*\{[^}]*$/,
decreaseIndentPattern: /^\s*\}/,
},
};
/**
* Theme contribution for stella-dsl tokens.
* These rules can be merged into an existing Monaco theme.
*/
export const stellaDslThemeRules: Monaco.editor.ITokenThemeRule[] = [
{ token: 'keyword.declaration.stella', foreground: '569CD6', fontStyle: 'bold' },
{ token: 'keyword.section.stella', foreground: 'C586C0' },
{ token: 'keyword.control.stella', foreground: 'C586C0' },
{ token: 'keyword.operator.stella', foreground: '569CD6' },
{ token: 'keyword.action.stella', foreground: 'DCDCAA' },
{ token: 'keyword.modifier.stella', foreground: '4EC9B0' },
{ token: 'support.function.stella', foreground: 'DCDCAA' },
{ token: 'support.function.vex.stella', foreground: 'DCDCAA' },
{ token: 'support.function.advisory.stella', foreground: 'DCDCAA' },
{ token: 'support.function.sbom.stella', foreground: 'DCDCAA' },
{ token: 'support.function.secret.stella', foreground: 'DCDCAA' },
{ token: 'support.function.ruby.stella', foreground: 'DCDCAA' },
{ token: 'variable.namespace.stella', foreground: '9CDCFE' },
{ token: 'constant.language.boolean.stella', foreground: '569CD6' },
{ token: 'constant.language.vex-status.stella', foreground: '4EC9B0' },
{ token: 'constant.language.severity.stella', foreground: 'CE9178' },
{ token: 'constant.language.reachability.stella', foreground: '4EC9B0' },
{ token: 'number.percentage.stella', foreground: 'B5CEA8' },
{ token: 'operator.arrow.stella', foreground: 'D4D4D4' },
{ token: 'operator.assignment.stella', foreground: 'D4D4D4' },
];
/**
* Registers the stella-dsl language with Monaco editor.
* Call this once during application initialization.
*
* @param monaco - Monaco editor namespace
*/
export function registerStellaDslLanguage(monaco: typeof Monaco): void {
// Register the language
monaco.languages.register({
id: STELLA_DSL_LANGUAGE_ID,
extensions: ['.stella'],
aliases: ['Stella DSL', 'stella-dsl', 'stella'],
mimetypes: ['text/x-stella-dsl'],
});
// Set the tokenizer
monaco.languages.setMonarchTokensProvider(
STELLA_DSL_LANGUAGE_ID,
stellaDslMonarchLanguage
);
// Set the language configuration
monaco.languages.setLanguageConfiguration(
STELLA_DSL_LANGUAGE_ID,
stellaDslLanguageConfiguration
);
}
/**
* Applies stella-dsl theme rules to an existing Monaco theme.
*
* @param monaco - Monaco editor namespace
* @param themeName - Name of the theme to extend
* @param baseTheme - Base theme to inherit from
*/
export function defineStellaDslTheme(
monaco: typeof Monaco,
themeName: string = 'stella-dsl-dark',
baseTheme: Monaco.editor.BuiltinTheme = 'vs-dark'
): void {
monaco.editor.defineTheme(themeName, {
base: baseTheme,
inherit: true,
rules: stellaDslThemeRules,
colors: {},
});
}

View File

@@ -0,0 +1,19 @@
/**
* Policy Studio feature module exports.
*
* This module provides:
* - Monaco editor language definition for stella-dsl
* - Policy API client service
* - Domain models for policies, simulations, and approvals
*
* @task UI-POLICY-20-001, UI-POLICY-20-002, UI-POLICY-20-003, UI-POLICY-20-004
*/
// Editor (Monaco language definition)
export * from './editor';
// Models
export * from './models';
// Services
export * from './services';

View File

@@ -0,0 +1,5 @@
/**
* Policy Studio models exports.
*/
export * from './policy.models';

View File

@@ -0,0 +1,363 @@
/**
* Policy Studio domain models.
*
* Models for policy packs, versions, simulations, and approval workflows.
*
* @task UI-POLICY-20-001, UI-POLICY-20-002, UI-POLICY-20-003
*/
/**
* Policy pack summary for list views.
*/
export interface PolicyPackSummary {
readonly id: string;
readonly name: string;
readonly description: string;
readonly version: string;
readonly status: PolicyPackStatus;
readonly createdAt: string;
readonly modifiedAt: string;
readonly createdBy: string;
readonly modifiedBy: string;
readonly tags: readonly string[];
}
/**
* Full policy pack with content.
*/
export interface PolicyPack {
readonly id: string;
readonly name: string;
readonly description: string;
readonly syntax: string;
readonly content: string;
readonly version: string;
readonly status: PolicyPackStatus;
readonly metadata: PolicyMetadata;
readonly createdAt: string;
readonly modifiedAt: string;
readonly createdBy: string;
readonly modifiedBy: string;
readonly tags: readonly string[];
readonly digest: string;
}
/**
* Policy pack status.
*/
export type PolicyPackStatus =
| 'draft'
| 'pending_review'
| 'in_review'
| 'approved'
| 'rejected'
| 'active'
| 'shadow'
| 'deprecated';
/**
* Policy metadata block.
*/
export interface PolicyMetadata {
readonly description?: string;
readonly tags?: readonly string[];
readonly author?: string;
readonly reviewers?: readonly string[];
}
/**
* Policy version history entry.
*/
export interface PolicyVersion {
readonly version: string;
readonly digest: string;
readonly status: PolicyPackStatus;
readonly createdAt: string;
readonly createdBy: string;
readonly changeDescription: string;
readonly isCurrent: boolean;
}
/**
* Policy lint result.
*/
export interface PolicyLintResult {
readonly valid: boolean;
readonly errors: readonly PolicyDiagnostic[];
readonly warnings: readonly PolicyDiagnostic[];
readonly info: readonly PolicyDiagnostic[];
}
/**
* Policy diagnostic (error, warning, or info).
*/
export interface PolicyDiagnostic {
readonly severity: 'error' | 'warning' | 'info';
readonly code: string;
readonly message: string;
readonly line: number;
readonly column: number;
readonly endLine?: number;
readonly endColumn?: number;
readonly source: string;
}
/**
* Policy compilation result.
*/
export interface PolicyCompilationResult {
readonly success: boolean;
readonly digest?: string;
readonly irPath?: string;
readonly diagnostics: readonly PolicyDiagnostic[];
}
/**
* Simulation request parameters.
*/
export interface SimulationRequest {
readonly policyId: string;
readonly policyVersion?: string;
readonly scope: SimulationScope;
readonly inputs: SimulationInputs;
readonly options?: SimulationOptions;
}
/**
* Simulation scope definition.
*/
export interface SimulationScope {
readonly components?: readonly string[];
readonly advisories?: readonly string[];
readonly sbomId?: string;
readonly environment?: string;
}
/**
* Simulation input data.
*/
export interface SimulationInputs {
readonly env?: Record<string, string>;
readonly signals?: SimulationSignals;
}
/**
* Simulation signals (trust, reachability, etc.).
*/
export interface SimulationSignals {
readonly trust_score?: number;
readonly reachability?: {
readonly state: 'reachable' | 'unreachable' | 'unknown';
readonly score: number;
};
readonly entropy_penalty?: number;
readonly uncertainty?: {
readonly level: 'U1' | 'U2' | 'U3';
};
}
/**
* Simulation options.
*/
export interface SimulationOptions {
readonly includeExplainTrace?: boolean;
readonly diffAgainstActive?: boolean;
readonly sealed?: boolean;
}
/**
* Simulation result.
*/
export interface SimulationResult {
readonly runId: string;
readonly policyId: string;
readonly policyVersion: string;
readonly status: 'completed' | 'failed' | 'timeout';
readonly summary: SimulationSummary;
readonly findings: readonly SimulatedFinding[];
readonly diff?: SimulationDiff;
readonly explainTrace?: readonly ExplainEntry[];
readonly executedAt: string;
readonly durationMs: number;
}
/**
* Simulation summary statistics.
*/
export interface SimulationSummary {
readonly totalFindings: number;
readonly byStatus: Record<string, number>;
readonly bySeverity: Record<string, number>;
readonly ruleHits: readonly RuleHitSummary[];
readonly vexWins: number;
readonly suppressions: number;
}
/**
* Rule hit summary.
*/
export interface RuleHitSummary {
readonly ruleName: string;
readonly hitCount: number;
readonly priority: number;
}
/**
* Simulated finding.
*/
export interface SimulatedFinding {
readonly componentPurl: string;
readonly advisoryId: string;
readonly status: string;
readonly severity: SeverityInfo;
readonly matchedRules: readonly string[];
readonly annotations: Record<string, string>;
}
/**
* Severity information.
*/
export interface SeverityInfo {
readonly band: 'critical' | 'high' | 'medium' | 'low' | 'none';
readonly score?: number;
readonly vector?: string;
}
/**
* Simulation diff against active policy.
*/
export interface SimulationDiff {
readonly added: readonly FindingChange[];
readonly removed: readonly FindingChange[];
readonly changed: readonly FindingChange[];
readonly statusDeltas: Record<string, number>;
readonly severityDeltas: Record<string, number>;
}
/**
* Finding change in diff.
*/
export interface FindingChange {
readonly componentPurl: string;
readonly advisoryId: string;
readonly before?: {
readonly status: string;
readonly severity: SeverityInfo;
};
readonly after?: {
readonly status: string;
readonly severity: SeverityInfo;
};
readonly reason: string;
}
/**
* Explain trace entry.
*/
export interface ExplainEntry {
readonly step: number;
readonly ruleName: string;
readonly priority: number;
readonly matched: boolean;
readonly inputs: Record<string, unknown>;
readonly outputs: Record<string, unknown>;
readonly rationale?: string;
}
/**
* Approval workflow state.
*/
export interface ApprovalWorkflow {
readonly policyId: string;
readonly policyVersion: string;
readonly status: ApprovalStatus;
readonly submittedAt: string;
readonly submittedBy: string;
readonly reviews: readonly ApprovalReview[];
readonly requiredApprovers: number;
readonly currentApprovers: number;
}
/**
* Approval status.
*/
export type ApprovalStatus =
| 'pending'
| 'in_review'
| 'approved'
| 'rejected'
| 'changes_requested';
/**
* Approval review entry.
*/
export interface ApprovalReview {
readonly reviewerId: string;
readonly reviewerName: string;
readonly decision: 'approve' | 'reject' | 'request_changes';
readonly comment: string;
readonly reviewedAt: string;
}
/**
* Policy run dashboard data.
*/
export interface PolicyRunDashboard {
readonly policyId: string;
readonly runs: readonly PolicyRunSummary[];
readonly ruleHeatmap: readonly RuleHeatmapEntry[];
readonly vexWinsByDay: readonly TimeSeriesEntry[];
readonly suppressionsByDay: readonly TimeSeriesEntry[];
}
/**
* Policy run summary.
*/
export interface PolicyRunSummary {
readonly runId: string;
readonly policyVersion: string;
readonly startedAt: string;
readonly completedAt: string;
readonly status: 'completed' | 'failed' | 'timeout';
readonly findingsCount: number;
readonly changedCount: number;
}
/**
* Rule heatmap entry.
*/
export interface RuleHeatmapEntry {
readonly ruleName: string;
readonly hitCount: number;
readonly lastHit: string;
readonly averageLatencyMs: number;
}
/**
* Time series data entry.
*/
export interface TimeSeriesEntry {
readonly date: string;
readonly value: number;
}
/**
* Policy submission request.
*/
export interface PolicySubmissionRequest {
readonly policyId: string;
readonly version: string;
readonly message: string;
readonly coverageResults?: string;
readonly simulationDiff?: string;
}
/**
* Policy promotion request.
*/
export interface PolicyPromotionRequest {
readonly policyId: string;
readonly version: string;
readonly targetEnvironment: string;
readonly reason: string;
}

View File

@@ -0,0 +1,5 @@
/**
* Policy Studio services exports.
*/
export { PolicyApiService } from './policy-api.service';

View File

@@ -0,0 +1,375 @@
/**
* Policy API client service.
*
* Provides methods for interacting with the Policy Gateway API:
* - Pack CRUD operations
* - Lint and compile
* - Simulation
* - Approval workflow
* - Run dashboards
*
* @task UI-POLICY-20-001, UI-POLICY-20-002, UI-POLICY-20-003, UI-POLICY-20-004
*/
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import type {
PolicyPackSummary,
PolicyPack,
PolicyVersion,
PolicyLintResult,
PolicyCompilationResult,
SimulationRequest,
SimulationResult,
ApprovalWorkflow,
ApprovalReview,
PolicyRunDashboard,
PolicySubmissionRequest,
PolicyPromotionRequest,
} from '../models/policy.models';
/**
* Policy API base path.
*/
const API_BASE = '/api/policy';
/**
* Policy API client service.
*/
@Injectable({ providedIn: 'root' })
export class PolicyApiService {
private readonly http = inject(HttpClient);
// ============================================================================
// Pack Management
// ============================================================================
/**
* List all policy packs.
*
* @param params - Optional filter parameters
*/
listPacks(params?: {
status?: string;
tag?: string;
search?: string;
limit?: number;
offset?: number;
}): Observable<PolicyPackSummary[]> {
let httpParams = new HttpParams();
if (params?.status) httpParams = httpParams.set('status', params.status);
if (params?.tag) httpParams = httpParams.set('tag', params.tag);
if (params?.search) httpParams = httpParams.set('search', params.search);
if (params?.limit) httpParams = httpParams.set('limit', params.limit.toString());
if (params?.offset) httpParams = httpParams.set('offset', params.offset.toString());
return this.http.get<PolicyPackSummary[]>(`${API_BASE}/packs`, { params: httpParams });
}
/**
* Get a single policy pack by ID.
*
* @param packId - Policy pack ID
* @param version - Optional specific version
*/
getPack(packId: string, version?: string): Observable<PolicyPack> {
let httpParams = new HttpParams();
if (version) httpParams = httpParams.set('version', version);
return this.http.get<PolicyPack>(`${API_BASE}/packs/${packId}`, { params: httpParams });
}
/**
* Create a new policy pack.
*
* @param pack - Policy pack data
*/
createPack(pack: {
name: string;
description: string;
content: string;
tags?: string[];
}): Observable<PolicyPack> {
return this.http.post<PolicyPack>(`${API_BASE}/packs`, pack);
}
/**
* Update an existing policy pack.
*
* @param packId - Policy pack ID
* @param pack - Updated policy pack data
*/
updatePack(
packId: string,
pack: {
name?: string;
description?: string;
content?: string;
tags?: string[];
}
): Observable<PolicyPack> {
return this.http.put<PolicyPack>(`${API_BASE}/packs/${packId}`, pack);
}
/**
* Delete a policy pack.
*
* @param packId - Policy pack ID
*/
deletePack(packId: string): Observable<void> {
return this.http.delete<void>(`${API_BASE}/packs/${packId}`);
}
/**
* Get version history for a policy pack.
*
* @param packId - Policy pack ID
*/
getVersionHistory(packId: string): Observable<PolicyVersion[]> {
return this.http.get<PolicyVersion[]>(`${API_BASE}/packs/${packId}/versions`);
}
/**
* Restore a previous version of a policy pack.
*
* @param packId - Policy pack ID
* @param version - Version to restore
*/
restoreVersion(packId: string, version: string): Observable<PolicyPack> {
return this.http.post<PolicyPack>(`${API_BASE}/packs/${packId}/versions/${version}/restore`, {});
}
// ============================================================================
// Lint and Compile
// ============================================================================
/**
* Lint policy content.
*
* @param content - Policy DSL content to lint
*/
lint(content: string): Observable<PolicyLintResult> {
return this.http.post<PolicyLintResult>(`${API_BASE}/lint`, { content });
}
/**
* Compile policy content.
*
* @param packId - Policy pack ID
* @param options - Compile options
*/
compile(
packId: string,
options?: { version?: string; includeIr?: boolean }
): Observable<PolicyCompilationResult> {
let httpParams = new HttpParams();
if (options?.version) httpParams = httpParams.set('version', options.version);
if (options?.includeIr) httpParams = httpParams.set('includeIr', 'true');
return this.http.post<PolicyCompilationResult>(
`${API_BASE}/packs/${packId}/compile`,
{},
{ params: httpParams }
);
}
// ============================================================================
// Simulation
// ============================================================================
/**
* Run a policy simulation.
*
* @param request - Simulation request
*/
simulate(request: SimulationRequest): Observable<SimulationResult> {
return this.http.post<SimulationResult>(`${API_BASE}/simulate`, request);
}
/**
* Get a simulation result by run ID.
*
* @param runId - Simulation run ID
*/
getSimulationResult(runId: string): Observable<SimulationResult> {
return this.http.get<SimulationResult>(`${API_BASE}/simulations/${runId}`);
}
/**
* List recent simulations for a policy.
*
* @param packId - Policy pack ID
* @param limit - Maximum results to return
*/
listSimulations(packId: string, limit: number = 10): Observable<SimulationResult[]> {
return this.http.get<SimulationResult[]>(
`${API_BASE}/packs/${packId}/simulations`,
{ params: new HttpParams().set('limit', limit.toString()) }
);
}
// ============================================================================
// Approval Workflow
// ============================================================================
/**
* Submit a policy for review.
*
* @param request - Submission request
*/
submitForReview(request: PolicySubmissionRequest): Observable<ApprovalWorkflow> {
return this.http.post<ApprovalWorkflow>(
`${API_BASE}/packs/${request.policyId}/submit`,
request
);
}
/**
* Get the current approval workflow state.
*
* @param packId - Policy pack ID
* @param version - Policy version
*/
getApprovalWorkflow(packId: string, version: string): Observable<ApprovalWorkflow> {
return this.http.get<ApprovalWorkflow>(
`${API_BASE}/packs/${packId}/versions/${version}/approval`
);
}
/**
* Add a review to the approval workflow.
*
* @param packId - Policy pack ID
* @param version - Policy version
* @param review - Review data
*/
addReview(
packId: string,
version: string,
review: {
decision: 'approve' | 'reject' | 'request_changes';
comment: string;
}
): Observable<ApprovalReview> {
return this.http.post<ApprovalReview>(
`${API_BASE}/packs/${packId}/versions/${version}/reviews`,
review
);
}
/**
* Promote a policy to a target environment.
* Requires interactive authentication (policy:promote scope).
*
* @param request - Promotion request
*/
promote(request: PolicyPromotionRequest): Observable<{ success: boolean; promotedAt: string }> {
return this.http.post<{ success: boolean; promotedAt: string }>(
`${API_BASE}/packs/${request.policyId}/promote`,
request
);
}
/**
* Activate a policy (switch from shadow to active mode).
*
* @param packId - Policy pack ID
* @param version - Policy version
*/
activate(packId: string, version: string): Observable<PolicyPack> {
return this.http.post<PolicyPack>(
`${API_BASE}/packs/${packId}/versions/${version}/activate`,
{}
);
}
/**
* Deprecate a policy pack.
*
* @param packId - Policy pack ID
* @param reason - Deprecation reason
*/
deprecate(packId: string, reason: string): Observable<PolicyPack> {
return this.http.post<PolicyPack>(
`${API_BASE}/packs/${packId}/deprecate`,
{ reason }
);
}
// ============================================================================
// Run Dashboards
// ============================================================================
/**
* Get policy run dashboard data.
*
* @param packId - Policy pack ID
* @param options - Dashboard options
*/
getRunDashboard(
packId: string,
options?: {
startDate?: string;
endDate?: string;
limit?: number;
}
): Observable<PolicyRunDashboard> {
let httpParams = new HttpParams();
if (options?.startDate) httpParams = httpParams.set('startDate', options.startDate);
if (options?.endDate) httpParams = httpParams.set('endDate', options.endDate);
if (options?.limit) httpParams = httpParams.set('limit', options.limit.toString());
return this.http.get<PolicyRunDashboard>(
`${API_BASE}/packs/${packId}/dashboard`,
{ params: httpParams }
);
}
/**
* Get rule heatmap data for a policy.
*
* @param packId - Policy pack ID
* @param days - Number of days to include (default 30)
*/
getRuleHeatmap(packId: string, days: number = 30): Observable<{ rules: Array<{
ruleName: string;
hitsByDay: Array<{ date: string; count: number }>;
}> }> {
return this.http.get<{ rules: Array<{
ruleName: string;
hitsByDay: Array<{ date: string; count: number }>;
}> }>(
`${API_BASE}/packs/${packId}/heatmap`,
{ params: new HttpParams().set('days', days.toString()) }
);
}
/**
* Export policy run results.
*
* @param packId - Policy pack ID
* @param format - Export format
* @param options - Export options
*/
exportResults(
packId: string,
format: 'json' | 'csv' | 'pdf',
options?: {
startDate?: string;
endDate?: string;
includeExplain?: boolean;
}
): Observable<Blob> {
let httpParams = new HttpParams().set('format', format);
if (options?.startDate) httpParams = httpParams.set('startDate', options.startDate);
if (options?.endDate) httpParams = httpParams.set('endDate', options.endDate);
if (options?.includeExplain) httpParams = httpParams.set('includeExplain', 'true');
return this.http.get(`${API_BASE}/packs/${packId}/export`, {
params: httpParams,
responseType: 'blob',
});
}
}