Add unit tests for Router configuration and transport layers
- 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:
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
129
src/Web/StellaOps.Web/package-lock.json
generated
129
src/Web/StellaOps.Web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
@@ -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 (0–1).' },
|
||||
{ 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 (0–1).' },
|
||||
{ label: 'signals.entropy_penalty', kind: 5, insertText: 'signals.entropy_penalty', documentation: 'Entropy penalty (0–0.3).' },
|
||||
{ label: 'signals.uncertainty.level', kind: 5, insertText: 'signals.uncertainty.level', documentation: 'Uncertainty level (U1–U3).' },
|
||||
{ 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 };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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: {},
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Policy Studio models exports.
|
||||
*/
|
||||
|
||||
export * from './policy.models';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Policy Studio services exports.
|
||||
*/
|
||||
|
||||
export { PolicyApiService } from './policy-api.service';
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user