diff --git a/calm/draft/2025-01/meta/core.json b/calm/draft/2025-01/meta/core.json index b60a9e64a..413e4b869 100644 --- a/calm/draft/2025-01/meta/core.json +++ b/calm/draft/2025-01/meta/core.json @@ -197,7 +197,7 @@ "network", "ldap", "webclient", - "data-assset" + "data-asset" ] }, "interacts-type": { diff --git a/calm/draft/2025-01/meta/flow.json b/calm/draft/2025-01/meta/flow.json index b734b2aed..97f0e650d 100644 --- a/calm/draft/2025-01/meta/flow.json +++ b/calm/draft/2025-01/meta/flow.json @@ -31,8 +31,7 @@ "sequence-number", "summary" ] - }, - "minItems": 1 + } }, "metadata": { "type": "array", @@ -63,7 +62,8 @@ "type": "array", "items": { "$ref": "#/defs/transition" - } + }, + "minItems": 1 }, "controls": { "$ref": "control.json#/defs/controls" diff --git a/shared/src/model/control-requirement.spec.ts b/shared/src/model/control-requirement.spec.ts new file mode 100644 index 000000000..29f6599f3 --- /dev/null +++ b/shared/src/model/control-requirement.spec.ts @@ -0,0 +1,35 @@ +import { CalmControlRequirement } from './control-requirement.js'; // Importing the model +import { CalmControlRequirementSchema } from '../types/control-requirement-types.js'; // Importing the schema + +// Mock data for testing +const controlRequirementData: CalmControlRequirementSchema = { + 'control-id': 'control-123', + name: 'Test Control Requirement', + description: 'This is a description of the test control requirement.' +}; + +describe('CalmControlRequirement', () => { + it('should create a CalmControlRequirement instance from JSON data', () => { + const controlRequirement = CalmControlRequirement.fromJson(controlRequirementData); + + expect(controlRequirement).toBeInstanceOf(CalmControlRequirement); + expect(controlRequirement.controlId).toBe('control-123'); + expect(controlRequirement.name).toBe('Test Control Requirement'); + expect(controlRequirement.description).toBe('This is a description of the test control requirement.'); + }); + + it('should handle missing description', () => { + const controlRequirementWithNoDescription: CalmControlRequirementSchema = { + 'control-id': 'control-124', + name: 'Control with No Description', + description: '' + }; + const controlRequirement = CalmControlRequirement.fromJson(controlRequirementWithNoDescription); + + expect(controlRequirement).toBeInstanceOf(CalmControlRequirement); + expect(controlRequirement.controlId).toBe('control-124'); + expect(controlRequirement.name).toBe('Control with No Description'); + expect(controlRequirement.description).toBe(''); + }); + +}); diff --git a/shared/src/model/control-requirement.ts b/shared/src/model/control-requirement.ts new file mode 100644 index 000000000..13ec09be5 --- /dev/null +++ b/shared/src/model/control-requirement.ts @@ -0,0 +1,17 @@ +import {CalmControlRequirementSchema} from '../types/control-requirement-types.js'; + +export class CalmControlRequirement { + constructor( + public controlId: string, + public name: string, + public description: string + ) {} + + static fromJson(data: CalmControlRequirementSchema): CalmControlRequirement { + return new CalmControlRequirement( + data['control-id'], + data['name'], + data['description'] + ); + } +} diff --git a/shared/src/model/control.spec.ts b/shared/src/model/control.spec.ts new file mode 100644 index 000000000..1b19733d7 --- /dev/null +++ b/shared/src/model/control.spec.ts @@ -0,0 +1,83 @@ +import { CalmControl, CalmControlDetail } from './control.js'; +import { CalmControlDetailSchema, CalmControlsSchema } from '../types/control-types.js'; + +const controlDetailData: CalmControlDetailSchema = { + 'control-requirement-url': 'https://example.com/requirement', + 'control-config-url': 'https://example.com/config' +}; + +const controlData: CalmControlsSchema = { + 'control-1': { + description: 'Test Control 1', + requirements: [controlDetailData] + }, + 'control-2': { + description: 'Test Control 2', + requirements: [controlDetailData] + } +}; + +describe('CalmControlDetail', () => { + it('should create a CalmControlDetail instance from JSON data', () => { + const controlDetail = CalmControlDetail.fromJson(controlDetailData); + + expect(controlDetail).toBeInstanceOf(CalmControlDetail); + expect(controlDetail.controlRequirementUrl).toBe('https://example.com/requirement'); + expect(controlDetail.controlConfigUrl).toBe('https://example.com/config'); + }); +}); + +describe('CalmControl', () => { + it('should create a CalmControl instance from JSON data', () => { + const controls = CalmControl.fromJson(controlData); + + expect(controls).toHaveLength(2); + expect(controls[0]).toBeInstanceOf(CalmControl); + expect(controls[0].controlId).toBe('control-1'); + expect(controls[0].description).toBe('Test Control 1'); + expect(controls[0].requirements).toHaveLength(1); + expect(controls[0].requirements[0]).toBeInstanceOf(CalmControlDetail); + expect(controls[0].requirements[0].controlRequirementUrl).toBe('https://example.com/requirement'); + expect(controls[0].requirements[0].controlConfigUrl).toBe('https://example.com/config'); + }); + + it('should handle an empty ControlsSchema', () => { + const emptyControls: CalmControlsSchema = {}; + const controls = CalmControl.fromJson(emptyControls); + + expect(controls).toHaveLength(0); + }); + + it('should handle missing requirements in Control', () => { + const controlWithNoRequirements: CalmControlsSchema = { + 'control-3': { + description: 'Control with no requirements', + requirements: [] + } + }; + const controls = CalmControl.fromJson(controlWithNoRequirements); + + expect(controls).toHaveLength(1); + expect(controls[0].requirements).toHaveLength(0); + }); + + it('should handle multiple controls and requirements', () => { + const controlWithMultipleRequirements: CalmControlsSchema = { + 'control-4': { + description: 'Control with multiple requirements', + requirements: [ + { 'control-requirement-url': 'https://example.com/requirement-1', 'control-config-url': 'https://example.com/config-1' }, + { 'control-requirement-url': 'https://example.com/requirement-2', 'control-config-url': 'https://example.com/config-2' } + ] + } + }; + + const controls = CalmControl.fromJson(controlWithMultipleRequirements); + + expect(controls).toHaveLength(1); + expect(controls[0].controlId).toBe('control-4'); + expect(controls[0].requirements).toHaveLength(2); + expect(controls[0].requirements[0].controlRequirementUrl).toBe('https://example.com/requirement-1'); + expect(controls[0].requirements[1].controlRequirementUrl).toBe('https://example.com/requirement-2'); + }); +}); diff --git a/shared/src/model/control.ts b/shared/src/model/control.ts new file mode 100644 index 000000000..eef790d8d --- /dev/null +++ b/shared/src/model/control.ts @@ -0,0 +1,35 @@ +import {CalmControlDetailSchema, CalmControlsSchema} from '../types/control-types.js'; + +export class CalmControlDetail { + constructor( + public controlRequirementUrl: string, + public controlConfigUrl: string + ) {} + + static fromJson(data: CalmControlDetailSchema): CalmControlDetail { + return new CalmControlDetail( + data['control-requirement-url'], + data['control-config-url'] + ); + } +} + +export class CalmControl { + constructor( + public controlId: string, + public description: string, + public requirements: CalmControlDetail[] + ) {} + + static fromJson(data: CalmControlsSchema): CalmControl[] { + if(!data) return []; + return Object.entries(data).map(([controlId, controlData]) => + new CalmControl( + controlId, + controlData.description, + controlData.requirements.map(CalmControlDetail.fromJson) + ) + ); + } + +} \ No newline at end of file diff --git a/shared/src/model/core.spec.ts b/shared/src/model/core.spec.ts new file mode 100644 index 000000000..e06df6478 --- /dev/null +++ b/shared/src/model/core.spec.ts @@ -0,0 +1,85 @@ +import { CalmCore } from './core.js'; +import { CalmCoreSchema } from '../types/core-types.js'; + +const coreData: CalmCoreSchema = { + nodes: [ + { + 'unique-id': 'node-001', + 'node-type': 'system', + name: 'Test Node', + description: 'This is a test node', + details: { + 'detailed-architecture': 'https://example.com/architecture', + 'required-pattern': 'https://example.com/pattern' + }, + interfaces: [{ 'unique-id': 'interface-001', hostname: 'localhost'}], + controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } }, + metadata: [{ key: 'value' }], + 'data-classification': 'Public', + 'run-as': 'admin', + instance: 'instance-1' + } + ], + relationships: [ + { + 'unique-id': 'relationship-001', + description: 'Test Relationship', + 'relationship-type': { + interacts: { + actor: 'actor-001', + nodes: ['node-001', 'node-002'] + } + }, + protocol: 'HTTP', + authentication: 'OAuth2', + metadata: [{ key: 'value' }], + controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } } + } + ], + metadata: [{ key: 'value' }], + controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } }, + flows: [] +}; + +describe('CalmCore', () => { + it('should create a CalmCore instance from CoreSchema data', () => { + const core = CalmCore.fromJson(coreData); + + expect(core).toBeInstanceOf(CalmCore); + expect(core.nodes).toHaveLength(1); + expect(core.relationships).toHaveLength(1); + expect(core.metadata).toEqual({ data: { key: 'value' } }); + expect(core.controls).toHaveLength(1); + expect(core.controls[0].controlId).toBe('control-001'); + expect(core.flows).toHaveLength(0); + }); + + it('should handle optional fields in CalmCore', () => { + const coreDataWithoutOptionalFields: CalmCoreSchema = { + nodes: [ + { + 'unique-id': 'node-002', + 'node-type': 'service', + name: 'Another Test Node', + description: 'Another test node description', + details: { + 'detailed-architecture': 'https://example.com/architecture-2', + 'required-pattern': 'https://example.com/pattern-2' + }, + interfaces: [{ 'unique-id': 'interface-001', hostname: 'localhost'}], + controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } }, + metadata: [{ key: 'value' }] + } + ], + metadata: [{ key: 'value' }], + controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } }, + }; + + const coreWithoutOptionalFields = CalmCore.fromJson(coreDataWithoutOptionalFields); + + expect(coreWithoutOptionalFields).toBeInstanceOf(CalmCore); + expect(coreWithoutOptionalFields.nodes).toHaveLength(1); + expect(coreWithoutOptionalFields.relationships).toHaveLength(0); + expect(coreWithoutOptionalFields.flows).toHaveLength(0); + }); +}); diff --git a/shared/src/model/core.ts b/shared/src/model/core.ts new file mode 100644 index 000000000..c4c131271 --- /dev/null +++ b/shared/src/model/core.ts @@ -0,0 +1,29 @@ +import { CalmNode } from './node.js'; +import { CalmRelationship } from './relationship.js'; +import { CalmFlow } from './flow.js'; +import { CalmControl } from './control.js'; +import { CalmMetadata } from './metadata.js'; +import { CalmCoreSchema } from '../types/core-types.js'; + +export class CalmCore { + constructor( + public nodes: CalmNode[], + public relationships: CalmRelationship[], + public metadata: CalmMetadata, + public controls: CalmControl[], + public flows: CalmFlow[] + ) {} + + static fromJson(data: CalmCoreSchema): CalmCore { + return new CalmCore( + data.nodes? data.nodes.map(CalmNode.fromJson) : [], + data.relationships? data.relationships.map(CalmRelationship.fromJson) : [], + data.metadata? CalmMetadata.fromJson(data.metadata) : new CalmMetadata({}), + data.controls? CalmControl.fromJson(data.controls) : [], + data.flows? data.flows.map(CalmFlow.fromJson) : [] + ); + } +} + +export { CalmCore as Architecture }; +export { CalmCore as Pattern }; diff --git a/shared/src/model/flow.spec.ts b/shared/src/model/flow.spec.ts new file mode 100644 index 000000000..006a1dbc3 --- /dev/null +++ b/shared/src/model/flow.spec.ts @@ -0,0 +1,72 @@ +import { CalmFlow, CalmFlowTransition } from './flow.js'; +import { CalmControl } from './control.js'; +import { CalmMetadata } from './metadata.js'; +import {CalmFlowSchema} from '../types/flow-types.js'; + +describe('CalmFlow', () => { + it('should create an instance with given properties', () => { + const transitions = [ + new CalmFlowTransition('rel-1', 1, 'First transition'), + new CalmFlowTransition('rel-2', 2, 'Second transition', 'destination-to-source') + ]; + + const controls = [ + new CalmControl('ctrl-1', 'Test Control 1', []), + new CalmControl('ctrl-2', 'Test Control 2', []) + ]; + + const metadata = new CalmMetadata({ key: 'value' }); + + const flow = new CalmFlow('flow-123', 'Test Flow', 'A test description', transitions, 'http://requirement.url', controls, metadata); + + expect(flow.uniqueId).toBe('flow-123'); + expect(flow.name).toBe('Test Flow'); + expect(flow.description).toBe('A test description'); + expect(flow.transitions).toHaveLength(2); + expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition); + expect(flow.controls).toBeDefined(); + expect(flow.controls).toHaveLength(2); + expect(flow.metadata).toBeInstanceOf(CalmMetadata); + }); + + it('should create an instance from JSON data', () => { + const jsonData: CalmFlowSchema = { + 'unique-id': 'flow-456', + 'name': 'JSON Flow', + 'description': 'Flow created from JSON', + 'transitions': [ + { + 'relationship-unique-id': 'rel-1', + 'sequence-number': 1, + 'summary': 'Transition 1' + }, + { + 'relationship-unique-id': 'rel-2', + 'sequence-number': 2, + 'summary': 'Transition 2', + 'direction': 'destination-to-source' // Explicitly providing direction + } + ], + 'requirement-url': 'http://json.requirement.url', + 'controls': { + 'ctrl-1': { 'description': 'JSON Control 1', 'requirements': [] } + }, + 'metadata': [{ 'key': 'value' }] + }; + + const flow = CalmFlow.fromJson(jsonData); + + + expect(flow).toBeInstanceOf(CalmFlow); + expect(flow.uniqueId).toBe('flow-456'); + expect(flow.name).toBe('JSON Flow'); + expect(flow.description).toBe('Flow created from JSON'); + expect(flow.transitions).toHaveLength(2); + expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition); + expect(flow.controls).toBeDefined(); + expect(flow.controls[0].controlId).toBe('ctrl-1'); + expect(flow.controls[0].description).toBe('JSON Control 1'); + expect(flow.metadata).toBeDefined(); + }); + +}); diff --git a/shared/src/model/flow.ts b/shared/src/model/flow.ts new file mode 100644 index 000000000..779103990 --- /dev/null +++ b/shared/src/model/flow.ts @@ -0,0 +1,48 @@ +import { CalmControl } from './control.js'; +import { CalmMetadata } from './metadata.js'; +import {CalmFlowSchema, CalmFlowTransitionSchema} from '../types/flow-types.js'; + +export type CalmFlowDirection = 'source-to-destination' | 'destination-to-source'; + +export class CalmFlow { + constructor( + public uniqueId: string, + public name: string, + public description: string, + public transitions: CalmFlowTransition[], + public requirementUrl: string, + public controls: CalmControl[], + public metadata: CalmMetadata + ) {} + + static fromJson(data: CalmFlowSchema): CalmFlow { + + return new CalmFlow( + data['unique-id'], + data.name, + data.description, + data.transitions.map(CalmFlowTransition.fromJson), + data['requirement-url'], + CalmControl.fromJson(data.controls), + CalmMetadata.fromJson(data.metadata) + ); + } +} + +export class CalmFlowTransition { + constructor( + public relationshipUniqueId: string, + public sequenceNumber: number, + public summary: string, + public direction: CalmFlowDirection = 'source-to-destination' + ) {} + + static fromJson(data: CalmFlowTransitionSchema): CalmFlowTransition { + return new CalmFlowTransition( + data['relationship-unique-id'], + data['sequence-number'], + data.summary, + data.direction || 'source-to-destination' + ); + } +} diff --git a/shared/src/model/interface.spec.ts b/shared/src/model/interface.spec.ts new file mode 100644 index 000000000..cefb27a1e --- /dev/null +++ b/shared/src/model/interface.spec.ts @@ -0,0 +1,165 @@ +import { + CalmInterface, + CalmHostPortInterface, + CalmHostnameInterface, + CalmPathInterface, + CalmOAuth2AudienceInterface, + CalmURLInterface, + CalmRateLimitInterface, + CalmContainerImageInterface, + CalmPortInterface, + CalmRateLimitKey +} from './interface.js'; +import { + CalmHostPortInterfaceSchema, + CalmHostnameInterfaceSchema, + CalmPathInterfaceSchema, + CalmOAuth2AudienceInterfaceSchema, + CalmURLInterfaceSchema, + CalmRateLimitInterfaceSchema, + CalmContainerImageInterfaceSchema, + CalmPortInterfaceSchema, + CalmRateLimitKeySchema +} from '../types/interface-types.js'; + +const hostPortData: CalmHostPortInterfaceSchema = { + 'unique-id': 'host-port-001', + host: 'localhost', + port: 8080 +}; + +const hostnameData: CalmHostnameInterfaceSchema = { + 'unique-id': 'hostname-001', + hostname: 'example.com' +}; + +const rateLimitKeyData: CalmRateLimitKeySchema = { + 'key-type': 'User', + 'static-value': 'user123' +}; + +const rateLimitData: CalmRateLimitInterfaceSchema = { + 'unique-id': 'rate-limit-001', + key: rateLimitKeyData, + time: 60, + 'time-unit': 'Seconds', + calls: 100 +}; + +describe('CalmInterface', () => { + it('should create a CalmHostPortInterface from JSON data', () => { + const calmInterface: CalmInterface = CalmInterface.fromJson(hostPortData); + + expect(calmInterface).toBeInstanceOf(CalmHostPortInterface); + const hostPortInterface = calmInterface as CalmHostPortInterface; + expect(hostPortInterface.uniqueId).toBe('host-port-001'); + expect(hostPortInterface.host).toBe('localhost'); + expect(hostPortInterface.port).toBe(8080); + }); + + it('should create a CalmHostnameInterface from JSON data', () => { + const hostnameInterface = CalmInterface.fromJson(hostnameData); + + expect(hostnameInterface).toBeInstanceOf(CalmHostnameInterface); + const hostInterface = hostnameInterface as CalmHostnameInterface; + expect(hostInterface.uniqueId).toBe('hostname-001'); + expect(hostInterface.hostname).toBe('example.com'); + }); + + it('should create a CalmRateLimitInterface from JSON data', () => { + const rateLimitInterface = CalmInterface.fromJson(rateLimitData); + + expect(rateLimitInterface).toBeInstanceOf(CalmRateLimitInterface); + const limitInterface = rateLimitInterface as CalmRateLimitInterface; + expect(limitInterface.uniqueId).toBe('rate-limit-001'); + expect(limitInterface.key).toBeInstanceOf(CalmRateLimitKey); + expect(limitInterface.time).toBe(60); + expect(limitInterface.timeUnit).toBe('Seconds'); + expect(limitInterface.calls).toBe(100); + }); +}); + +describe('CalmRateLimitKey', () => { + it('should create a CalmRateLimitKey from JSON data', () => { + const rateLimitKey = CalmRateLimitKey.fromJson(rateLimitKeyData); + + expect(rateLimitKey).toBeInstanceOf(CalmRateLimitKey); + expect(rateLimitKey.keyType).toBe('User'); + expect(rateLimitKey.staticValue).toBe('user123'); + }); +}); + +describe('CalmPortInterface', () => { + it('should create a CalmPortInterface from JSON data', () => { + const portInterfaceData: CalmPortInterfaceSchema = { + 'unique-id': 'port-001', + port: 8080 + }; + const portInterface = CalmInterface.fromJson(portInterfaceData); + + expect(portInterface).toBeInstanceOf(CalmPortInterface); + const port = portInterface as CalmPortInterface; + expect(port.uniqueId).toBe('port-001'); + expect(port.port).toBe(8080); + }); +}); + +describe('CalmOAuth2AudienceInterface', () => { + it('should create a CalmOAuth2AudienceInterface from JSON data', () => { + const oauth2AudienceData: CalmOAuth2AudienceInterfaceSchema = { + 'unique-id': 'oauth2-001', + audiences: ['audience1', 'audience2'] + }; + const oauth2AudienceInterface = CalmInterface.fromJson(oauth2AudienceData); + + expect(oauth2AudienceInterface).toBeInstanceOf(CalmOAuth2AudienceInterface); + const oauth2Interface = oauth2AudienceInterface as CalmOAuth2AudienceInterface; + expect(oauth2Interface.uniqueId).toBe('oauth2-001'); + expect(oauth2Interface.audiences).toEqual(['audience1', 'audience2']); + }); +}); + +describe('CalmContainerImageInterface', () => { + it('should create a CalmContainerImageInterface from JSON data', () => { + const containerImageData: CalmContainerImageInterfaceSchema = { + 'unique-id': 'container-001', + image: 'docker/image-name' + }; + const containerImageInterface = CalmInterface.fromJson(containerImageData); + + expect(containerImageInterface).toBeInstanceOf(CalmContainerImageInterface); + const containerInterface = containerImageInterface as CalmContainerImageInterface; + expect(containerInterface.uniqueId).toBe('container-001'); + expect(containerInterface.image).toBe('docker/image-name'); + }); +}); + +describe('CalmPathInterface', () => { + it('should create a CalmPathInterface from JSON data', () => { + const pathInterfaceData: CalmPathInterfaceSchema = { + 'unique-id': 'path-001', + path: '/api/v1/resource' + }; + const pathInterface = CalmInterface.fromJson(pathInterfaceData); + + expect(pathInterface).toBeInstanceOf(CalmPathInterface); + const path = pathInterface as CalmPathInterface; + expect(path.uniqueId).toBe('path-001'); + expect(path.path).toBe('/api/v1/resource'); + }); +}); + +describe('CalmURLInterface', () => { + it('should create a CalmURLInterface from JSON data', () => { + const urlInterfaceData: CalmURLInterfaceSchema = { + 'unique-id': 'url-001', + url: 'https://example.com' + }; + const urlInterface = CalmInterface.fromJson(urlInterfaceData); + + expect(urlInterface).toBeInstanceOf(CalmURLInterface); + const url = urlInterface as CalmURLInterface; + expect(url.uniqueId).toBe('url-001'); + expect(url.url).toBe('https://example.com'); + }); +}); diff --git a/shared/src/model/interface.ts b/shared/src/model/interface.ts new file mode 100644 index 000000000..acb3d2c12 --- /dev/null +++ b/shared/src/model/interface.ts @@ -0,0 +1,145 @@ +import { + CalmContainerImageInterfaceSchema, + CalmHostnameInterfaceSchema, + CalmHostPortInterfaceSchema, + CalmInterfaceTypeSchema, + CalmNodeInterfaceSchema, + CalmOAuth2AudienceInterfaceSchema, + CalmPathInterfaceSchema, CalmPortInterfaceSchema, + CalmRateLimitInterfaceSchema, CalmRateLimitKeySchema, + CalmURLInterfaceSchema +} from '../types/interface-types.js'; + +export class CalmInterface { + constructor(public uniqueId: string) {} + + static fromJson(data: CalmInterfaceTypeSchema): CalmInterface { + if ('host' in data && 'port' in data) { + return CalmHostPortInterface.fromJson(data as CalmHostPortInterfaceSchema); + } else if ('hostname' in data) { + return CalmHostnameInterface.fromJson(data as CalmHostnameInterfaceSchema); + } else if ('path' in data) { + return CalmPathInterface.fromJson(data as CalmPathInterfaceSchema); + } else if ('audiences' in data) { + return CalmOAuth2AudienceInterface.fromJson(data as CalmOAuth2AudienceInterfaceSchema); + } else if ('url' in data) { + return CalmURLInterface.fromJson(data as CalmURLInterfaceSchema); + } else if ('key' in data) { + return CalmRateLimitInterface.fromJson(data as CalmRateLimitInterfaceSchema); + } else if ('image' in data) { + return CalmContainerImageInterface.fromJson(data as CalmContainerImageInterfaceSchema); + } else if ('port' in data) { + return CalmPortInterface.fromJson(data as CalmPortInterfaceSchema); + } else { + throw new Error('Unknown interface type'); + } + } +} + +export class CalmNodeInterface { + constructor(public node: string, public interfaces: string[]) {} + + static fromJson(data: CalmNodeInterfaceSchema): CalmNodeInterface { + return new CalmNodeInterface(data.node, data.interfaces); + } +} + +export class CalmHostPortInterface extends CalmInterface { + constructor(public uniqueId: string, public host: string, public port: number) { + super(uniqueId); + } + + static fromJson(data: CalmHostPortInterfaceSchema): CalmHostPortInterface { + return new CalmHostPortInterface(data['unique-id'], data.host, data.port); + } +} + +export class CalmHostnameInterface extends CalmInterface { + constructor(public uniqueId: string, public hostname: string) { + super(uniqueId); + } + + static fromJson(data: CalmHostnameInterfaceSchema): CalmHostnameInterface { + return new CalmHostnameInterface(data['unique-id'], data.hostname); + } +} + +export class CalmPathInterface extends CalmInterface { + constructor(public uniqueId: string, public path: string) { + super(uniqueId); + } + + static fromJson(data: CalmPathInterfaceSchema): CalmPathInterface { + return new CalmPathInterface(data['unique-id'], data.path); + } +} + +export class CalmOAuth2AudienceInterface extends CalmInterface { + constructor(public uniqueId: string, public audiences: string[]) { + super(uniqueId); + } + + static fromJson(data: CalmOAuth2AudienceInterfaceSchema): CalmOAuth2AudienceInterface { + return new CalmOAuth2AudienceInterface(data['unique-id'], data.audiences); + } +} + +export class CalmURLInterface extends CalmInterface { + constructor(public uniqueId: string, public url: string) { + super(uniqueId); + } + + static fromJson(data: CalmURLInterfaceSchema): CalmURLInterface { + return new CalmURLInterface(data['unique-id'], data.url); + } +} + +export class CalmRateLimitInterface extends CalmInterface { + constructor( + public uniqueId: string, + public key: CalmRateLimitKey, + public time: number, + public timeUnit: 'Seconds' | 'Minutes' | 'Hours', + public calls: number + ) { + super(uniqueId); + } + + static fromJson(data: CalmRateLimitInterfaceSchema): CalmRateLimitInterface { + return new CalmRateLimitInterface( + data['unique-id'], + CalmRateLimitKey.fromJson(data.key), + data.time, + data['time-unit'], + data.calls + ); + } +} + +export class CalmContainerImageInterface extends CalmInterface { + constructor(public uniqueId: string, public image: string) { + super(uniqueId); + } + + static fromJson(data: CalmContainerImageInterfaceSchema): CalmContainerImageInterface { + return new CalmContainerImageInterface(data['unique-id'], data.image); + } +} + +export class CalmPortInterface extends CalmInterface { + constructor(public uniqueId: string, public port: number) { + super(uniqueId); + } + + static fromJson(data: CalmPortInterfaceSchema): CalmPortInterface { + return new CalmPortInterface(data['unique-id'], data.port); + } +} + +export class CalmRateLimitKey { + constructor(public keyType: 'User' | 'IP' | 'Global' | 'Header' | 'OAuth2Client', public staticValue: string) {} + + static fromJson(data: CalmRateLimitKeySchema): CalmRateLimitKey { + return new CalmRateLimitKey(data['key-type'], data['static-value']); + } +} diff --git a/shared/src/model/metadata.spec.ts b/shared/src/model/metadata.spec.ts new file mode 100644 index 000000000..869320c25 --- /dev/null +++ b/shared/src/model/metadata.spec.ts @@ -0,0 +1,49 @@ +import { CalmMetadata } from './metadata.js'; +import { CalmMetadataSchema } from '../types/metadata-types.js'; + +const metadataData: CalmMetadataSchema = [ + { key1: 'value1', key2: 'value2' }, + { key3: 'value3', key4: 'value4' } +]; + +describe('CalmMetadata', () => { + it('should create a CalmMetadata instance from JSON data', () => { + const metadata = CalmMetadata.fromJson(metadataData); + + expect(metadata).toBeInstanceOf(CalmMetadata); + expect(metadata.data).toEqual({ + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'value4' + }); + }); + + it('should flatten metadata correctly when there are multiple entries', () => { + const metadata = CalmMetadata.fromJson(metadataData); + + expect(metadata.data).toEqual({ + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'value4' + }); + }); + + it('should handle empty metadata array', () => { + const metadata = CalmMetadata.fromJson([]); + + expect(metadata).toBeInstanceOf(CalmMetadata); + expect(metadata.data).toEqual({}); + }); + + it('should handle single metadata entry', () => { + const singleMetadata: CalmMetadataSchema = [{ key1: 'value1' }]; + const metadata = CalmMetadata.fromJson(singleMetadata); + + expect(metadata).toBeInstanceOf(CalmMetadata); + expect(metadata.data).toEqual({ + key1: 'value1' + }); + }); +}); diff --git a/shared/src/model/metadata.ts b/shared/src/model/metadata.ts new file mode 100644 index 000000000..e3efac834 --- /dev/null +++ b/shared/src/model/metadata.ts @@ -0,0 +1,13 @@ +import { CalmMetadataSchema } from '../types/metadata-types.js'; + +export class CalmMetadata { + constructor(public data: Record) {} + + static fromJson(data: CalmMetadataSchema): CalmMetadata { + const flattenedData = data.reduce((acc, curr) => { + return { ...acc, ...curr }; + }, {} as Record); + + return new CalmMetadata(flattenedData); + } +} diff --git a/shared/src/model/node.spec.ts b/shared/src/model/node.spec.ts new file mode 100644 index 000000000..c99ad3799 --- /dev/null +++ b/shared/src/model/node.spec.ts @@ -0,0 +1,106 @@ +import { CalmNode, CalmNodeDetails } from './node.js'; +import {CalmNodeSchema} from '../types/core-types.js'; + +const nodeData: CalmNodeSchema = { + 'unique-id': 'node-001', + 'node-type': 'system', + name: 'Test Node', + description: 'This is a test node', + details: { + 'detailed-architecture': 'https://example.com/architecture', + 'required-pattern': 'https://example.com/pattern' + }, + interfaces: [ + { 'unique-id': 'interface-001', host: 'localhost', port: 8080 }, + { 'unique-id': 'interface-002', port: 8080 } + ], + controls: { + 'control-001': { + description: 'Test control', + requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] + } + }, + metadata: [{ key: 'value' }], + 'data-classification': 'Public', + 'run-as': 'admin', + instance: 'instance-1' +}; + + +describe('CalmNodeDetails', () => { + it('should create a CalmNodeDetails instance from JSON data', () => { + const nodeDetails = CalmNodeDetails.fromJson(nodeData.details); + + expect(nodeDetails).toBeInstanceOf(CalmNodeDetails); + expect(nodeDetails.detailedArchitecture).toBe('https://example.com/architecture'); + expect(nodeDetails.requiredPattern).toBe('https://example.com/pattern'); + }); +}); + +describe('CalmNode', () => { + it('should create a CalmNode instance from JSON data', () => { + const node = CalmNode.fromJson(nodeData); + + expect(node).toBeInstanceOf(CalmNode); + expect(node.uniqueId).toBe('node-001'); + expect(node.nodeType).toBe('system'); + expect(node.name).toBe('Test Node'); + expect(node.description).toBe('This is a test node'); + expect(node.details).toBeInstanceOf(CalmNodeDetails); + expect(node.details.detailedArchitecture).toBe('https://example.com/architecture'); + expect(node.details.requiredPattern).toBe('https://example.com/pattern'); + expect(node.interfaces).toHaveLength(2); + expect(node.controls).toHaveLength(1); + expect(node.controls[0].controlId).toBe('control-001'); + expect(node.metadata).toEqual({ data: { key: 'value' } }); + expect(node.dataClassification).toBe('Public'); + expect(node.runAs).toBe('admin'); + expect(node.instance).toBe('instance-1'); + }); + + it('should handle optional fields in CalmNode', () => { + const nodeDataWithoutOptionalFields: CalmNodeSchema = { + 'unique-id': 'node-002', + 'node-type': 'service', + name: 'Another Test Node', + description: 'Another test node description', + details: { + 'detailed-architecture': 'https://example.com/architecture-2', + 'required-pattern': 'https://example.com/pattern-2' + }, + interfaces: [{ 'unique-id': 'interface-002', port: 8080 }], + controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } }, + metadata: [{ key: 'value' }] + }; + + const nodeWithoutOptionalFields = CalmNode.fromJson(nodeDataWithoutOptionalFields); + + expect(nodeWithoutOptionalFields).toBeInstanceOf(CalmNode); + expect(nodeWithoutOptionalFields.uniqueId).toBe('node-002'); + expect(nodeWithoutOptionalFields.runAs).toBeUndefined(); + expect(nodeWithoutOptionalFields.instance).toBeUndefined(); + }); + + it('should handle empty interfaces, controls, and metadata', () => { + const nodeDataWithEmptyFields: CalmNodeSchema = { + 'unique-id': 'node-003', + 'node-type': 'database', + name: 'Database Node', + description: 'Node with empty fields', + details: { + 'detailed-architecture': 'https://example.com/architecture-3', + 'required-pattern': 'https://example.com/pattern-3' + }, + interfaces: [], + controls: {}, + metadata: [] + }; + + const nodeWithEmptyFields = CalmNode.fromJson(nodeDataWithEmptyFields); + + expect(nodeWithEmptyFields).toBeInstanceOf(CalmNode); + expect(nodeWithEmptyFields.interfaces).toHaveLength(0); + expect(nodeWithEmptyFields.controls).toHaveLength(0); + expect(nodeWithEmptyFields.metadata).toEqual({ data: {}}); + }); +}); diff --git a/shared/src/model/node.ts b/shared/src/model/node.ts new file mode 100644 index 000000000..7e3517501 --- /dev/null +++ b/shared/src/model/node.ts @@ -0,0 +1,55 @@ +import {CalmInterface} from './interface.js'; +import {CalmControl} from './control.js'; +import {CalmMetadata} from './metadata.js'; +import {CalmNodeDetailsSchema, CalmNodeSchema} from '../types/core-types.js'; + +export type CalmNodeType = 'actor' | 'ecosystem' | 'system' | 'service' | 'database' | 'network' | 'ldap' | 'webclient' | 'data-asset'; + +export type CalmDataClassification = 'Public' | 'Confidential' | 'Highly Restricted' | 'MNPI' | 'PII'; + +export class CalmNodeDetails { + constructor( + public detailedArchitecture: string, + public requiredPattern: string + ){} + static fromJson(data: CalmNodeDetailsSchema): CalmNodeDetails { + return new CalmNodeDetails( + data['detailed-architecture'], + data['required-pattern'] + ); + } +} + + +export class CalmNode { + constructor( + public uniqueId: string, + public nodeType: CalmNodeType, + public name: string, + public description: string, + public details: CalmNodeDetails, + public interfaces: CalmInterface[], + public controls: CalmControl[], + public metadata: CalmMetadata, + public dataClassification?: CalmDataClassification, + public runAs?: string, + public instance?: string, + + ) {} + + static fromJson(data: CalmNodeSchema): CalmNode { + return new CalmNode( + data['unique-id'], + data['node-type'], + data.name, + data.description, + data.details ? CalmNodeDetails.fromJson(data.details) : new CalmNodeDetails('', ''), + data.interfaces ? data.interfaces.map(CalmInterface.fromJson) : [], + data.controls ? CalmControl.fromJson(data.controls) : [], + data.metadata ? CalmMetadata.fromJson(data.metadata): new CalmMetadata({}), + data['data-classification'], + data['run-as'], + data.instance, + ); + } +} diff --git a/shared/src/model/relationship.spec.ts b/shared/src/model/relationship.spec.ts new file mode 100644 index 000000000..0c6916416 --- /dev/null +++ b/shared/src/model/relationship.spec.ts @@ -0,0 +1,117 @@ +import { CalmRelationship, CalmInteractsType, CalmConnectsType, CalmDeployedInType, CalmComposedOfType } from './relationship.js'; +import { CalmRelationshipSchema } from '../types/core-types.js'; +import { CalmNodeInterface } from './interface.js'; + +const relationshipData: CalmRelationshipSchema = { + 'unique-id': 'relationship-001', + description: 'Test Relationship', + 'relationship-type': { + interacts: { + actor: 'actor-001', + nodes: ['node-001', 'node-002'] + } + }, + protocol: 'HTTP', + authentication: 'OAuth2', + metadata: [{ key: 'value' }], + controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } } +}; + +describe('CalmRelationship', () => { + it('should create a CalmRelationship instance from JSON data', () => { + const relationship = CalmRelationship.fromJson(relationshipData); + + expect(relationship).toBeInstanceOf(CalmRelationship); + expect(relationship.uniqueId).toBe('relationship-001'); + expect(relationship.description).toBe('Test Relationship'); + expect(relationship.protocol).toBe('HTTP'); + expect(relationship.authentication).toBe('OAuth2'); + expect(relationship.metadata).toEqual({ data: { key: 'value' } }); + expect(relationship.controls).toHaveLength(1); + expect(relationship.controls[0].controlId).toBe('control-001'); + expect(relationship.relationshipType).toBeInstanceOf(CalmInteractsType); + + const interactsRelationship = relationship.relationshipType as CalmInteractsType; + + expect(interactsRelationship.actor).toBe('actor-001'); + expect(interactsRelationship.nodes).toEqual(['node-001', 'node-002']); + }); + + it('should handle a CalmConnectsType relationship type correctly', () => { + const connectsRelationshipData: CalmRelationshipSchema = { + 'unique-id': 'relationship-002', + description: 'Connects Relationship', + 'relationship-type': { + connects: { + source: { 'node': 'node-001', interfaces: ['interface-001'] }, + destination: { 'node': 'node-002', interfaces: ['interface-002'] } + } + }, + protocol: 'TLS', + authentication: 'Basic', + metadata: [{ key: 'value2' }], + controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } } + }; + + const relationship = CalmRelationship.fromJson(connectsRelationshipData); + + expect(relationship).toBeInstanceOf(CalmRelationship); + expect(relationship.relationshipType).toBeInstanceOf(CalmConnectsType); + + const connectsRelationship = relationship.relationshipType as CalmConnectsType; + expect(connectsRelationship.source).toBeInstanceOf(CalmNodeInterface); + expect(connectsRelationship.destination).toBeInstanceOf(CalmNodeInterface); + }); + + it('should handle a CalmDeployedInType relationship type correctly', () => { + const deployedInRelationshipData: CalmRelationshipSchema = { + 'unique-id': 'relationship-003', + description: 'Deployed In Relationship', + 'relationship-type': { + 'deployed-in': { + container: 'container-001', + nodes: ['node-001', 'node-002'] + } + }, + protocol: 'AMQP', + authentication: 'Certificate', + metadata: [{ key: 'value3' }], + controls: { 'control-003': { description: 'Test control 3', requirements: [{ 'control-requirement-url': 'https://example.com/requirement3', 'control-config-url': 'https://example.com/config3' }] } } + }; + + const relationship = CalmRelationship.fromJson(deployedInRelationshipData); + + expect(relationship).toBeInstanceOf(CalmRelationship); + expect(relationship.relationshipType).toBeInstanceOf(CalmDeployedInType); + + const deployedInRelationship = relationship.relationshipType as CalmDeployedInType; + expect(deployedInRelationship.container).toBe('container-001'); + expect(deployedInRelationship.nodes).toEqual(['node-001', 'node-002']); + }); + + it('should handle a CalmComposedOfType relationship type correctly', () => { + const composedOfRelationshipData: CalmRelationshipSchema = { + 'unique-id': 'relationship-004', + description: 'Composed Of Relationship', + 'relationship-type': { + 'composed-of': { + container: 'container-002', + nodes: ['node-003', 'node-004'] + } + }, + protocol: 'TCP', + authentication: 'OAuth2', + metadata: [{ key: 'value4' }], + controls: { 'control-004': { description: 'Test control 4', requirements: [{ 'control-requirement-url': 'https://example.com/requirement4', 'control-config-url': 'https://example.com/config4' }] } } + }; + + const relationship = CalmRelationship.fromJson(composedOfRelationshipData); + + expect(relationship).toBeInstanceOf(CalmRelationship); + expect(relationship.relationshipType).toBeInstanceOf(CalmComposedOfType); + + const composedOfRelationship = relationship.relationshipType as CalmDeployedInType; + expect(composedOfRelationship.container).toBe('container-002'); + expect(composedOfRelationship.nodes).toEqual(['node-003', 'node-004']); + }); +}); diff --git a/shared/src/model/relationship.ts b/shared/src/model/relationship.ts new file mode 100644 index 000000000..a0409a6a4 --- /dev/null +++ b/shared/src/model/relationship.ts @@ -0,0 +1,97 @@ +import { CalmMetadata } from './metadata.js'; +import { CalmControl } from './control.js'; +import { + CalmComposedOfRelationshipSchema, + CalmConnectsRelationshipSchema, CalmDeployedInRelationshipSchema, + CalmInteractsRelationshipSchema, + CalmRelationshipSchema, + CalmRelationshipTypeSchema +} from '../types/core-types.js'; +import {CalmNodeInterface} from './interface.js'; + + +export class CalmRelationship { + constructor( + public uniqueId: string, + public relationshipType: CalmRelationshipType, + public metadata: CalmMetadata, + public controls: CalmControl[], + public description?: string, + public protocol?: string , + public authentication?: string + + ) {} + + static fromJson(data: CalmRelationshipSchema): CalmRelationship { + return new CalmRelationship( + data['unique-id'], + CalmRelationship.deriveRelationshipType(data['relationship-type']), + data.metadata ? CalmMetadata.fromJson(data.metadata) : new CalmMetadata({}), + CalmControl.fromJson(data.controls), + data.description, + data.protocol, + data.authentication + ); + } + + + + static deriveRelationshipType(data: CalmRelationshipTypeSchema): CalmRelationshipType { + if (data.interacts) { + return CalmInteractsType.fromJson(data.interacts); + } else if (data.connects) { + return CalmConnectsType.fromJson(data.connects); + } else if (data['deployed-in']) { + return CalmDeployedInType.fromJson(data['deployed-in']); + } else if (data['composed-of']) { + return CalmComposedOfType.fromJson(data['composed-of']); + } else { + throw new Error('Invalid relationship type data'); + } + } +} + +export abstract class CalmRelationshipType {} + +export class CalmInteractsType extends CalmRelationshipType { + constructor(public actor: string, public nodes: string[]) { + super(); + } + + static fromJson(data: CalmInteractsRelationshipSchema): CalmInteractsType { + return new CalmInteractsType(data.actor, data.nodes); + } +} + +export class CalmConnectsType extends CalmRelationshipType { + constructor(public source: CalmNodeInterface, public destination: CalmNodeInterface) { + super(); + } + + static fromJson(data: CalmConnectsRelationshipSchema): CalmConnectsType { + return new CalmConnectsType( + CalmNodeInterface.fromJson(data.source), + CalmNodeInterface.fromJson(data.destination) + ); + } +} + +export class CalmDeployedInType extends CalmRelationshipType { + constructor(public container: string, public nodes: string[]) { + super(); + } + + static fromJson(data: CalmDeployedInRelationshipSchema): CalmDeployedInType { + return new CalmDeployedInType(data.container, data.nodes); + } +} + +export class CalmComposedOfType extends CalmRelationshipType { + constructor(public container: string, public nodes: string[]) { + super(); + } + + static fromJson(data: CalmComposedOfRelationshipSchema): CalmComposedOfType { + return new CalmComposedOfType(data.container, data.nodes); + } +} diff --git a/shared/src/parser/parser.e2e.spec.ts b/shared/src/parser/parser.e2e.spec.ts new file mode 100644 index 000000000..b6be62877 --- /dev/null +++ b/shared/src/parser/parser.e2e.spec.ts @@ -0,0 +1,51 @@ +import {CalmParser} from './parser.js'; +import {CalmCore} from '../model/core.js'; +import {CalmConnectsType, CalmInteractsType} from '../model/relationship.js'; + +describe('CalmParser Integration Tests verifying traderx example and model fromJson', () => { + let calmCore: CalmCore; + let calmParser: CalmParser; + + beforeAll(async () => { + calmParser = new CalmParser(); + calmCore = calmParser.parse('../calm/samples/2024-12/traderx/traderx.json'); + }); + + test('should parse nodes correctly', () => { + expect(calmCore.nodes).toBeDefined(); + expect(Array.isArray(calmCore.nodes)).toBe(true); + expect(calmCore.nodes.length).toBe(14); + + const firstNode = calmCore.nodes[0]; + expect(firstNode.uniqueId).toBe('traderx-system'); + expect(firstNode.nodeType).toBe('system'); + expect(firstNode.name).toBe('TraderX'); + expect(firstNode.description).toBe('Simple Trading System'); + }); + + test('should parse interacts relationships correctly and cast to InteractsType', () => { + const interactsRel = calmCore.relationships.find( + (r) => r.uniqueId === 'trader-executes-trades' + ); + expect(interactsRel).toBeDefined(); + expect(interactsRel.description).toBe('Executes Trades'); + + const interactsType = interactsRel.relationshipType as CalmInteractsType; + expect(interactsType.actor).toBe('traderx-trader'); + expect(Array.isArray(interactsType.nodes)).toBe(true); + expect(interactsType.nodes).toContain('web-client'); + }); + + test('should parse connects relationships correctly and cast to ConnectsType', () => { + const connectsRel = calmCore.relationships.find( + (r) => r.uniqueId === 'web-client-uses-web-gui' + ); + expect(connectsRel).toBeDefined(); + expect(connectsRel.protocol).toBe('HTTPS'); + + const connectsType = connectsRel.relationshipType as CalmConnectsType; + expect(connectsType.source.node).toBe('web-client'); + expect(connectsType.destination.node).toBe('web-gui-process'); + }); + +}); diff --git a/shared/src/parser/parser.ts b/shared/src/parser/parser.ts new file mode 100644 index 000000000..0ae7b5b2c --- /dev/null +++ b/shared/src/parser/parser.ts @@ -0,0 +1,23 @@ +import { CalmCore } from '../model/core.js'; +import fs from 'fs'; +import { initLogger } from '../logger.js'; +import {CalmCoreSchema} from '../types/core-types.js'; + +export class CalmParser { + + private static logger = initLogger(process.env.DEBUG === 'true'); + + parse(coreCalmFilePath: string): CalmCore { + const logger = CalmParser.logger; + try { + const data = fs.readFileSync(coreCalmFilePath, 'utf8'); + const dereferencedData: CalmCoreSchema = JSON.parse(data); // TODO: this needs to use SchemaDirectory to parse the other documents e.g. flows. + dereferencedData.flows = []; // If this ends up being string documents then this will break CalmFlow.fromJson + dereferencedData.controls = {}; // If this ends up being string documents then this will break CalmControl.fromJson + return CalmCore.fromJson(dereferencedData); + } catch (error) { + logger.error('Failed to parse calm.json:', error); + throw error; + } + } +} diff --git a/shared/src/types.ts b/shared/src/types.ts index 466db8c77..090f35465 100644 --- a/shared/src/types.ts +++ b/shared/src/types.ts @@ -6,7 +6,7 @@ export interface CALMArchitecture { export type NodeType = 'actor' | 'system' | 'service' | 'database' | 'network' | 'ldap' | 'dataclient'; export interface CALMNode { - name: string, + name: string, class?: string, 'unique-id': string, 'node-type': NodeType, diff --git a/shared/src/types/control-requirement-types.ts b/shared/src/types/control-requirement-types.ts new file mode 100644 index 000000000..623e99cb6 --- /dev/null +++ b/shared/src/types/control-requirement-types.ts @@ -0,0 +1,5 @@ +export type CalmControlRequirementSchema = { + 'control-id': string; + name: string; + description: string; +}; diff --git a/shared/src/types/control-types.ts b/shared/src/types/control-types.ts new file mode 100644 index 000000000..bdee1c13b --- /dev/null +++ b/shared/src/types/control-types.ts @@ -0,0 +1,13 @@ +export type CalmControlDetailSchema = { + 'control-requirement-url': string; + 'control-config-url': string; +}; + +export type CalmControlSchema = { + description: string; + requirements: CalmControlDetailSchema[]; +}; + +export type CalmControlsSchema = { + [controlId: string]: CalmControlSchema; +}; diff --git a/shared/src/types/core-types.ts b/shared/src/types/core-types.ts new file mode 100644 index 000000000..8df81ea42 --- /dev/null +++ b/shared/src/types/core-types.ts @@ -0,0 +1,87 @@ +import { + CalmContainerImageInterfaceSchema, + CalmHostnameInterfaceSchema, + CalmHostPortInterfaceSchema, CalmInterfaceTypeSchema, + CalmNodeInterfaceSchema, + CalmOAuth2AudienceInterfaceSchema, + CalmPathInterfaceSchema, CalmPortInterfaceSchema, + CalmRateLimitInterfaceSchema, + CalmURLInterfaceSchema +} from './interface-types.js'; +import { CalmControlsSchema } from './control-types.js'; +import { CalmMetadataSchema } from './metadata-types.js'; +import { CalmFlowSchema } from './flow-types.js'; + +export type CalmNodeTypeSchema = 'actor' | 'ecosystem' | 'system' | 'service' | 'database' | 'network' | 'ldap' | 'webclient' | 'data-asset'; +export type CalmDataClassificationSchema = 'Public' | 'Confidential' | 'Highly Restricted' | 'MNPI' | 'PII'; +export type CalmProtocolSchema = 'HTTP' | 'HTTPS' | 'FTP' | 'SFTP' | 'JDBC' | 'WebSocket' | 'SocketIO' | 'LDAP' | 'AMQP' | 'TLS' | 'mTLS' | 'TCP'; +export type CalmAuthenticationSchema = 'Basic' | 'OAuth2' | 'Kerberos' | 'SPNEGO' | 'Certificate'; + +export type CalmNodeDetailsSchema = { + 'detailed-architecture': string; + 'required-pattern': string; +}; + +export type CalmNodeSchema = { + 'unique-id': string; + 'node-type': CalmNodeTypeSchema; + name: string; + description: string; + details?: CalmNodeDetailsSchema; + 'data-classification'?: CalmDataClassificationSchema; + 'run-as'?: string; + instance?: string; + interfaces?: (CalmInterfaceTypeSchema | CalmHostPortInterfaceSchema | CalmHostnameInterfaceSchema | CalmPathInterfaceSchema | CalmOAuth2AudienceInterfaceSchema | CalmURLInterfaceSchema | CalmRateLimitInterfaceSchema | CalmContainerImageInterfaceSchema | CalmPortInterfaceSchema)[]; + controls?: CalmControlsSchema; + metadata?: CalmMetadataSchema; +}; + +export type CalmInteractsRelationshipSchema = { + actor: string; + nodes: string[]; +}; + +export type CalmConnectsRelationshipSchema = { + source: CalmNodeInterfaceSchema; + destination: CalmNodeInterfaceSchema; +}; + +export type CalmDeployedInRelationshipSchema = { + container: string; + nodes: string[]; +}; + +export type CalmComposedOfRelationshipSchema = { + container: string; + nodes: string[]; +}; + +export type CalmRelationshipTypeSchema = { + interacts?: CalmInteractsRelationshipSchema; + connects?: CalmConnectsRelationshipSchema; + 'deployed-in'?: CalmDeployedInRelationshipSchema; + 'composed-of'?: CalmComposedOfRelationshipSchema; +}; + + +export type CalmRelationshipSchema = { + 'unique-id': string; + 'description'?: string; + 'relationship-type': CalmRelationshipTypeSchema; + protocol?: CalmProtocolSchema; + authentication?: CalmAuthenticationSchema; + metadata?: CalmMetadataSchema; + controls?: CalmControlsSchema; +}; + +//TODO: There is no required section. +export type CalmCoreSchema = { + nodes?: CalmNodeSchema[]; + relationships?: CalmRelationshipSchema[]; + metadata?: CalmMetadataSchema; + controls?: CalmControlsSchema; + flows?: CalmFlowSchema[]; +}; + +export type CalmArchitectureSchema = CalmCoreSchema +export type CalmPatternSchema = CalmCoreSchema diff --git a/shared/src/types/flow-types.ts b/shared/src/types/flow-types.ts new file mode 100644 index 000000000..f24e49979 --- /dev/null +++ b/shared/src/types/flow-types.ts @@ -0,0 +1,21 @@ +import {CalmControlsSchema} from './control-types.js'; +import {CalmMetadataSchema} from './metadata-types.js'; + +export type CalmFlowTransitionDirectionSchema = 'source-to-destination' | 'destination-to-source'; + +export type CalmFlowTransitionSchema = { + 'relationship-unique-id': string; + 'sequence-number': number; + summary: string; + direction?: CalmFlowTransitionDirectionSchema; +}; + +export type CalmFlowSchema = { + 'unique-id': string; + name: string; + description: string; + 'requirement-url'?: string; + transitions: CalmFlowTransitionSchema[]; + controls?: CalmControlsSchema; + metadata?: CalmMetadataSchema; +}; diff --git a/shared/src/types/interface-types.ts b/shared/src/types/interface-types.ts new file mode 100644 index 000000000..eaab9e093 --- /dev/null +++ b/shared/src/types/interface-types.ts @@ -0,0 +1,50 @@ + +export type CalmInterfaceTypeSchema = { + 'unique-id': string; +}; + +export type CalmNodeInterfaceSchema = { + node: string; + interfaces: string[]; +}; + +export type CalmHostPortInterfaceSchema = CalmInterfaceTypeSchema & { + host: string; + port: number; +}; + +export type CalmHostnameInterfaceSchema = CalmInterfaceTypeSchema & { + hostname: string; +}; + +export type CalmPathInterfaceSchema = CalmInterfaceTypeSchema & { + path: string; +}; + +export type CalmOAuth2AudienceInterfaceSchema = CalmInterfaceTypeSchema & { + audiences: string[]; +}; + +export type CalmURLInterfaceSchema = CalmInterfaceTypeSchema & { + url: string; +}; + +export type CalmRateLimitInterfaceSchema = CalmInterfaceTypeSchema & { + key: CalmRateLimitKeySchema; + time: number; + 'time-unit': 'Seconds' | 'Minutes' | 'Hours'; //TODO: change to use time-unit schema + calls: number; +}; + +export type CalmContainerImageInterfaceSchema = CalmInterfaceTypeSchema & { + image: string; +}; + +export type CalmPortInterfaceSchema = CalmInterfaceTypeSchema & { + port: number; +}; + +export type CalmRateLimitKeySchema = { + 'key-type': 'User' | 'IP' | 'Global' | 'Header' | 'OAuth2Client'; + 'static-value': string; +}; diff --git a/shared/src/types/metadata-types.ts b/shared/src/types/metadata-types.ts new file mode 100644 index 000000000..61b287d13 --- /dev/null +++ b/shared/src/types/metadata-types.ts @@ -0,0 +1 @@ +export type CalmMetadataSchema = Record[]; diff --git a/shared/src/types/units-types.ts b/shared/src/types/units-types.ts new file mode 100644 index 000000000..9d3c0d3a2 --- /dev/null +++ b/shared/src/types/units-types.ts @@ -0,0 +1,6 @@ +export type CalmTimeUnitSchema = { + unit: 'nanoseconds' | 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'; + value: number; +}; + +export type CalmCronExpressionSchema = string;