diff --git a/README.md b/README.md index 59052047..020fd25a 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Built on STIX 2.1 compliance, it uses Zod schemas and TypeScript types to ensure - **Type-Safe Data Parsing**: ADM validates STIX 2.1 bundles using Zod schemas, ensuring data model compliance and type safety. - **Easy Relationship Navigation**: Each object instance contains pointers to related objects, simplifying the process of navigating between techniques, tactics, and other ATT&CK elements. -- **Supports Multiple Data Sources**: Load ATT&CK datasets from different sources, including GitHub, local files, URLs, and TAXII 2.1 servers (more data sources in development). +- **Supports Multiple Content Origins**: Load ATT&CK datasets from different content origins, including GitHub, local files, URLs, and TAXII 2.1 servers (more content origins in development). - Parsing, validation, and serialization of ATT&CK data - ES6 classes for object-oriented data manipulation -## Supported Data Sources +## Supported Content Origins -- **`attack`**: Load ATT&CK data from the official MITRE ATT&CK STIX 2.1 GitHub repository. This serves as the source of truth for MITRE ATT&CK content. +- **`mitre`**: Load ATT&CK data from the official MITRE ATT&CK STIX 2.1 GitHub repository. This serves as the source of truth for MITRE ATT&CK content. - **`file`**: Load ATT&CK data from a local JSON file containing a STIX 2.1 bundle. - **`url`**: Load ATT&CK data from a URL endpoint serving STIX 2.1 content. - **`taxii`**: (Coming soon) Load ATT&CK data from a TAXII 2.1 server. @@ -90,17 +90,17 @@ For most users, we recommend: Example of loading the latest ATT&CK data: ```javascript -import { registerDataSource, loadDataModel, DataSourceRegistration } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; -const dataSource = new DataSourceRegistration({ - source: 'attack', +const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'strict' }); -const dataSource = await registerDataSource(dataSource); -const attackEnterpriseLatest = loadDataModel(dataSource); +const dataSourceId = await registerContentOrigin(contentOrigin); +const attackEnterpriseLatest = loadDataModel(dataSourceId); ``` For more details on version compatibility, see the [Compatibility Guide](./COMPATIBILITY.md). @@ -132,21 +132,21 @@ For additional context about the ATT&CK specification, please refer to the [ATT& Here's an example script that demonstrates how to use the ADM library to load ATT&CK data from the official MITRE ATT&CK GitHub repository: ```typescript -import { registerDataSource, loadDataModel, DataSourceRegistration } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; (async () => { - // Instantiating a DataSourceRegistration object will validate that the data source is accessible and readable - const dataSource = new DataSourceRegistration({ - source: 'attack', // Built-in index to retrieve ATT&CK content from the official MITRE ATT&CK STIX 2.1 GitHub repository + // Instantiating a ContentOriginRegistration object will validate that the content origin is accessible and readable + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', // Built-in index to retrieve ATT&CK content from the official MITRE ATT&CK STIX 2.1 GitHub repository domain: 'enterprise-attack', version: '15.1', // Omitting 'version' will default to the latest version available in the repository parsingMode: 'relaxed' // 'strict' or 'relaxed' - 'relaxed' mode will attempt to parse and serialize data even if it contains errors or warnings }); try { - // Register the data source and retrieve the unique ID - const uuid = await registerDataSource(dataSource); + // Register the content origin and retrieve the unique ID + const uuid = await registerContentOrigin(contentOrigin); if (uuid) { // Load the dataset using the unique ID const attackEnterpriseLatest = loadDataModel(uuid); @@ -217,7 +217,7 @@ For more detailed examples, please refer to the [examples](./examples/README.md) ## How It Works -1. **Data Registration**: Datasets are registered via `registerDataSource`. You specify the source of the data (e.g., `attack`, `file`, `url`, `taxii`) and provide any necessary options (such as `domain` and `version` for ATT&CK datasets). This function returns a unique identifier for the registered data source. +1. **Content Origin Registration**: Datasets are registered via `registerContentOrigin`. You specify the content origin (e.g., `mitre`, `file`, `url`, `taxii`) and provide any necessary options (such as `domain` and `version` for ATT&CK datasets). This function returns a unique identifier for the registered content origin. 2. **Data Loading**: The `loadDataModel` function is used to load registered data models by their unique identifier. 3. **Parsing and Validation**: Once the data is loaded, it is parsed by Zod schemas, ensuring that the data conforms to the expected STIX 2.1 specification. 4. **Serialization**: Valid objects are converted into TypeScript class instances, allowing for type-safe interaction and relationship navigation. diff --git a/docs/USAGE.md b/docs/USAGE.md index 8d5dadba..a30f07d0 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -9,7 +9,7 @@ The ATT&CK Data Model (ADM) TypeScript API provides a structured and type-safe w - **Type-Safe Data Parsing**: Validates STIX 2.1 data using Zod schemas, ensuring compliance with the ATT&CK Data Model. - **Object-Oriented Interface**: Provides ES6 class wrappers for ATT&CK objects, enabling intuitive interaction and relationship navigation. - **Relationship Mapping**: Automatically processes relationships between objects, allowing easy traversal of the ATT&CK data model. -- **Flexible Data Sources**: Supports loading data from various sources, including the official MITRE ATT&CK GitHub repository, local files, URLs, and TAXII 2.1 servers (some data sources are under development). +- **Flexible Content Origins**: Supports loading data from various content origins, including the official MITRE ATT&CK GitHub repository, local files, URLs, and TAXII 2.1 servers (some content origins are under development). ## Installation @@ -65,7 +65,7 @@ When installed, the library has the following directory structure: │ ├── sdo │ ├── smo │ └── sro -├── data-sources +├── content-origins ├── errors └── schemas ├── common @@ -84,7 +84,7 @@ Each sub-package serves a specific purpose: - **`classes`**: Contains ES6 class wrappers for ATT&CK objects, providing methods for relationship navigation and data manipulation. - **`common`**: Base classes and shared components. - **`sdo`**, **`smo`**, **`sro`**: Class implementations corresponding to the schemas. -- **`data-sources`**: Modules for loading ATT&CK data from various sources. +- **`content-origins`**: Modules for loading ATT&CK data from various content origins. - **`errors`**: Custom error classes used throughout the library. ### Hierarchical Structure @@ -92,8 +92,7 @@ Each sub-package serves a specific purpose: The library is designed with a hierarchical structure. Every directory exports its modules through an `index.ts` file, creating a clear and organized namespace. The top-level `index.ts` file exports all components, allowing for straightforward imports: ```typescript -export * from './classes/index.js'; -export * from './data-sources/index.js'; +export * from './api/index.js'; export * from './errors/index.js'; export * from './schemas/index.js'; export * from './main.js'; @@ -210,20 +209,20 @@ console.log(attackDataModel.campaigns); // Access campaigns ### Initializing with Data -To use the `AttackDataModel`, you need to load it with data from a data source: +To use the `AttackDataModel`, you need to load it with data from a content origin: ```typescript -import { registerDataSource, loadDataModel, DataSource } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; (async () => { - const dataSource = new DataSource({ - source: 'attack', + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed', }); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const attackDataModel = loadDataModel(uuid); // Now you can interact with the data model @@ -289,13 +288,13 @@ console.log(campaign.name); const techniques = campaign.getTechniques(); ``` -## Data Sources +## Content Origins -The library supports loading data from various sources through the `DataSource` class. +The library supports loading data from various content origins through the `ContentOriginRegistration` class. -### Supported Data Sources +### Supported Content Origins -- **`attack`**: Official MITRE ATT&CK STIX 2.1 GitHub repository. +- **`mitre`**: Official MITRE ATT&CK STIX 2.1 GitHub repository. - **`file`**: (Coming soon) Local JSON files containing STIX 2.1 bundles. - **`url`**: (Coming soon) URLs serving STIX 2.1 content. - **`taxii`**: (Coming soon) TAXII 2.1 servers. @@ -303,17 +302,17 @@ The library supports loading data from various sources through the `DataSource` ### Loading Data from the ATT&CK GitHub Repository ```typescript -import { registerDataSource, loadDataModel, DataSource } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; (async () => { - const dataSource = new DataSource({ - source: 'attack', + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed', }); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const attackDataModel = loadDataModel(uuid); // Access ATT&CK objects @@ -400,18 +399,18 @@ try { ## Advanced Usage -### Custom Data Sources +### Custom Content Origins -You can create custom data sources by extending the `DataSource` class or by providing your own data loading logic. +You can create custom content origins by extending the `ContentOriginRegistration` class or by providing your own data loading logic. ```typescript import { DataSource } from '@mitre-attack/attack-data-model'; -class CustomDataSource extends DataSource { +class CustomContentOrigin extends ContentOriginRegistration { // Implement custom data loading logic } -const customDataSource = new CustomDataSource({ +const customContentOrigin = new CustomContentOrigin({ source: 'custom', // ... other options }); diff --git a/docusaurus/docs/how-to-guides/manage-data-sources.mdx b/docusaurus/docs/how-to-guides/manage-data-sources.mdx index df79d617..79ff8940 100644 --- a/docusaurus/docs/how-to-guides/manage-data-sources.mdx +++ b/docusaurus/docs/how-to-guides/manage-data-sources.mdx @@ -1,12 +1,12 @@ import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice'; -# How to Manage Data Sources +# How to Manage Content Origins -**Switch between different ATT&CK data sources efficiently** +**Switch between different ATT&CK content origins efficiently** -This guide shows you how to manage multiple ATT&CK data sources, switch between different versions, and work with local files, URLs, and the official repository. +This guide shows you how to manage multiple ATT&CK content origins, switch between different versions, and work with local files, URLs, and the official repository. ## Problem Scenarios @@ -15,15 +15,15 @@ Use this guide when you need to: - Switch between different ATT&CK versions for compatibility testing - Load ATT&CK data from local files instead of the internet - Fetch data from custom URLs or mirrors -- Manage multiple data sources in a production application -- Cache and reuse data sources efficiently +- Manage multiple content origins in a production application +- Cache and reuse content origins efficiently ## Switch Between ATT&CK Versions ### Compare Multiple Versions ```typescript -import { registerDataSource, loadDataModel, DataSource } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; async function compareVersions() { const versions = ['15.0', '15.1']; @@ -31,14 +31,14 @@ async function compareVersions() { // Load multiple versions for (const version of versions) { - const dataSource = new DataSource({ - source: 'attack', + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: version, parsingMode: 'relaxed' }); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); models[version] = loadDataModel(uuid); } @@ -55,7 +55,7 @@ async function compareVersions() { ```typescript // Omit version to get the latest available const latestDataSource = new DataSource({ - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', // No version specified = latest parsingMode: 'relaxed' @@ -130,7 +130,7 @@ async function loadFromUrl() { ```typescript async function loadWithAuth() { - const dataSource = new DataSource({ + const contentOrigin = new ContentOriginRegistration({ source: 'url', url: 'https://private-server.com/attack-data.json', requestOptions: { @@ -142,7 +142,7 @@ async function loadWithAuth() { parsingMode: 'strict' }); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); return loadDataModel(uuid); } ``` @@ -157,7 +157,7 @@ class AttackDataManager { async registerSource(name: string, config: any): Promise { const dataSource = new DataSource(config); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); this.dataSources.set(name, uuid); return uuid; } @@ -173,14 +173,14 @@ class AttackDataManager { async setupCommonSources() { // Enterprise latest await this.registerSource('enterprise-latest', { - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', parsingMode: 'relaxed' }); // Enterprise v15.0 await this.registerSource('enterprise-v15', { - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', version: '15.0', parsingMode: 'relaxed' @@ -188,7 +188,7 @@ class AttackDataManager { // Mobile latest await this.registerSource('mobile-latest', { - source: 'attack', + source: 'mitre', domain: 'mobile-attack', parsingMode: 'relaxed' }); @@ -212,13 +212,13 @@ async function loadWithFallback() { const fallbackSources = [ // Try latest first { - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', parsingMode: 'relaxed' }, // Fallback to specific version { - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed' @@ -234,7 +234,7 @@ async function loadWithFallback() { for (const config of fallbackSources) { try { const dataSource = new DataSource(config); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const model = loadDataModel(uuid); console.log(`Successfully loaded from source: ${config.source}`); @@ -256,7 +256,7 @@ async function loadWithFallback() { async function validateDataSource(config: any): Promise { try { const dataSource = new DataSource(config); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const model = loadDataModel(uuid); // Basic validation checks @@ -303,7 +303,7 @@ class CachedDataManager { // Load fresh data console.log('🌐 Loading fresh data'); const dataSource = new DataSource(config); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const model = loadDataModel(uuid); // Cache the result @@ -330,7 +330,7 @@ function getDataSourceConfig(): any { switch (environment) { case 'production': return { - source: 'attack', + source: 'mitre', domain: 'enterprise-attack', version: '15.1', // Pin version in production parsingMode: 'strict' // Strict validation in production @@ -370,7 +370,7 @@ async function loadWithMonitoring(config: any) { console.log('📡 Starting data source load:', config.source); const dataSource = new DataSource(config); - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); const model = loadDataModel(uuid); const loadTime = Date.now() - startTime; diff --git a/docusaurus/docs/reference/api/data-sources.mdx b/docusaurus/docs/reference/api/data-sources.mdx index 7a81b017..3ff5b2e1 100644 --- a/docusaurus/docs/reference/api/data-sources.mdx +++ b/docusaurus/docs/reference/api/data-sources.mdx @@ -1,24 +1,24 @@ import WorkInProgressNotice from '@site/src/components/WorkInProgressNotice'; -# DataSource +# ContentOriginRegistration -**Data source configuration and registration for loading ATT&CK datasets** +**Content origin configuration and registration for loading ATT&CK datasets** -The `DataSource` class defines where and how to load ATT&CK data. It supports multiple source types including the official ATT&CK repository, local files, remote URLs, and TAXII servers. +The `ContentOriginRegistration` class defines where and how to load ATT&CK data. It supports multiple content origin types including the official ATT&CK repository, local files, remote URLs, and TAXII servers. ## Constructor ```typescript -new DataSource(options: DataSourceOptions) +new ContentOriginRegistration(options: ContentOriginOptions) ``` -### DataSourceOptions Interface +### ContentOriginOptions Interface ```typescript -interface DataSourceOptions { - source: 'attack' | 'file' | 'url' | 'taxii'; +interface ContentOriginOptions { + source: 'mitre' | 'file' | 'url' | 'taxii'; parsingMode?: 'strict' | 'relaxed'; // Attack source options @@ -45,13 +45,13 @@ interface DataSourceOptions { ## Source Types -### Attack Repository Source +### MITRE Repository Source Load data from the official MITRE ATT&CK STIX 2.1 repository. ```typescript -const dataSource = new DataSource({ - source: 'attack', +const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'strict' @@ -62,7 +62,7 @@ const dataSource = new DataSource({ | Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| -| `source` | `'attack'` | ✅ | - | Specifies ATT&CK repository source | +| `source` | `'mitre'` | ✅ | - | Specifies MITRE ATT&CK repository source | | `domain` | `'enterprise-attack'` \| `'mobile-attack'` \| `'ics-attack'` | ✅ | - | ATT&CK domain to load | | `version` | `string` | ❌ | `'latest'` | Specific version (e.g., '15.1') or 'latest' | | `parsingMode` | `'strict'` \| `'relaxed'` | ❌ | `'strict'` | Validation strictness | @@ -85,7 +85,7 @@ const dataSource = new DataSource({ Load data from local STIX 2.1 bundle files. ```typescript -const dataSource = new DataSource({ +const contentOrigin = new ContentOriginRegistration({ source: 'file', file: '/path/to/enterprise-attack.json', parsingMode: 'relaxed' @@ -129,7 +129,7 @@ const dataSource = new DataSource({ Load data from remote URLs serving STIX 2.1 content. ```typescript -const dataSource = new DataSource({ +const contentOrigin = new ContentOriginRegistration({ source: 'url', url: 'https://example.com/attack-data.json', timeout: 30000, @@ -162,7 +162,7 @@ const dataSource = new DataSource({ Load data from TAXII 2.1 servers. ```typescript -const dataSource = new DataSource({ +const contentOrigin = new ContentOriginRegistration({ source: 'taxii', server: 'https://cti-taxii.mitre.org', collection: 'attack-patterns', @@ -217,38 +217,38 @@ parsingMode: 'relaxed' ## Registration and Loading -### registerDataSource() +### registerContentOrigin() -Validates and registers a data source for use. +Validates and registers a content origin for use. ```typescript -async function registerDataSource(dataSource: DataSource): Promise +async function registerContentOrigin(contentOrigin: ContentOriginRegistration): Promise ``` **Parameters**: -- `dataSource` - Configured DataSource instance +- `contentOrigin` - Configured ContentOriginRegistration instance **Returns**: -- `string` - UUID for the registered data source on success +- `string` - UUID for the registered content origin on success - `null` - Registration failed **Example**: ```typescript -import { registerDataSource } from '@mitre-attack/attack-data-model'; +import { registerContentOrigin } from '@mitre-attack/attack-data-model'; -const dataSource = new DataSource({ - source: 'attack', +const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1' }); try { - const uuid = await registerDataSource(dataSource); + const uuid = await registerContentOrigin(contentOrigin); if (uuid) { - console.log(`Data source registered: ${uuid}`); + console.log(`Content origin registered: ${uuid}`); } else { console.error('Registration failed'); } diff --git a/examples/sdo/technique-impl.example.ts b/examples/sdo/technique-impl.example.ts new file mode 100644 index 00000000..e1ce4686 --- /dev/null +++ b/examples/sdo/technique-impl.example.ts @@ -0,0 +1,129 @@ +import { z } from "zod/v4"; +import { type TechniqueCls, TechniqueImpl } from "../../src/api/sdo/technique.impl.js"; +import { techniqueSchema } from "../../src/schemas/sdo/technique.schema.js"; + +/*************************************************************************************************** */ +// Example 1: Valid Technique +/*************************************************************************************************** */ +const validEnterpriseTechnique = { + "modified": "2024-02-02T19:04:35.389Z", + "name": "Data Obfuscation", + "description": "Adversaries may obfuscate command and control traffic to make it more difficult to detect.(Citation: Bitdefender FunnyDream Campaign November 2020) Command and control (C2) communications are hidden (but not necessarily encrypted) in an attempt to make the content more difficult to discover or decipher and to make the communication less conspicuous and hide commands from being seen. This encompasses many methods, such as adding junk data to protocol traffic, using steganography, or impersonating legitimate protocols. ", + "kill_chain_phases": [ + { + "kill_chain_name": "mitre-attack", + "phase_name": "command-and-control" + } + ], + "x_mitre_deprecated": false, + "x_mitre_domains": [ + "enterprise-attack" + ], + "x_mitre_is_subtechnique": false, + "x_mitre_platforms": [ + "Linux", + "macOS", + "Windows" + ], + "x_mitre_version": "1.1", + "type": "attack-pattern", + "id": "attack-pattern--ad255bfe-a9e6-4b52-a258-8d3462abe842", + "created": "2017-05-31T21:30:18.931Z", + "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "revoked": false, + "external_references": [ + { + "source_name": "mitre-attack", + "url": "https://attack.mitre.org/techniques/T1001", + "external_id": "T1001" + } + ], + "object_marking_refs": [ + "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" + ], + "x_mitre_attack_spec_version": "3.2.0", + "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", + "spec_version": "2.1" +}; + +console.log("\nExample 1 - Valid Technique:"); +console.log(`SUCCESS ${techniqueSchema.safeParse(validEnterpriseTechnique).success}`) + +let technique: TechniqueCls +try { + const invalidTechnique = { ...validEnterpriseTechnique, id: 'foobar' } + technique = new TechniqueImpl(invalidTechnique); +} catch (err) { + console.error('Could not initialize class instance due to Zod error:') + console.error(z.prettifyError(err)); + // Could not initialize class instance due to Zod error: + // ✖ Invalid STIX Identifier: must comply with format 'type--UUIDv4' + // → at id + // ✖ Invalid STIX Identifier for STIX object: contains invalid STIX type 'foobar' + // → at id + // ✖ Invalid STIX Identifier for STIX object: contains invalid UUIDv4 format + // → at id + // ✖ Invalid STIX Identifier: must start with 'attack-pattern--' + // → at id + process.exit() +} + +console.log(technique.id); +// attack-pattern--ad255bfe-a9e6-4b52-a258-8d3462abe842 + +console.log(typeof technique.id); +// string + +console.log(technique.spec_version); +// 2.1 + +console.log(technique.external_references); +// [ +// { +// source_name: 'mitre-attack', +// url: 'https://attack.mitre.org/techniques/T1001', +// external_id: 'T1001' +// } +// ] + +console.log(technique.getAttackId()); +// T1001 + +console.log(technique.getDisplayName()); +// T1001: Data Obfuscation + +console.log(technique.equals(new TechniqueImpl(validEnterpriseTechnique))) +// true + +console.log(Object.keys(technique)) +/** + [ + 'id', + 'type', + 'spec_version', + 'created', + 'modified', + 'created_by_ref', + 'revoked', + 'external_references', + 'object_marking_refs', + 'name', + 'x_mitre_attack_spec_version', + 'x_mitre_version', + 'x_mitre_deprecated', + 'kill_chain_phases', + 'description', + 'x_mitre_platforms', + 'x_mitre_is_subtechnique', + 'x_mitre_domains', + 'x_mitre_modified_by_ref', + '_subTechniques', + '_tactics', + '_mitigations', + '_logSources', + '_relatedTechniques', + '_targetAssets', + '_detectingDataComponents' + ] + */ + diff --git a/src/classes/attack-data-model.ts b/src/api/attack-data-model.ts similarity index 100% rename from src/classes/attack-data-model.ts rename to src/api/attack-data-model.ts diff --git a/src/classes/common/attack-object.impl.ts b/src/api/common/attack-object.impl.ts similarity index 89% rename from src/classes/common/attack-object.impl.ts rename to src/api/common/attack-object.impl.ts index fd300d85..24d6d2b7 100644 --- a/src/classes/common/attack-object.impl.ts +++ b/src/api/common/attack-object.impl.ts @@ -14,7 +14,7 @@ export class AttackBaseImpl { /** * Returns the object that revoked this object. */ - getRevokedBy(): AnyAttackObject | undefined { + getRevokedBy() { return this.revokedBy; } } diff --git a/src/classes/common/index.ts b/src/api/common/index.ts similarity index 56% rename from src/classes/common/index.ts rename to src/api/common/index.ts index 574d6ea6..5f31fd8d 100644 --- a/src/classes/common/index.ts +++ b/src/api/common/index.ts @@ -1 +1,2 @@ +export * from './validated.js'; export * from './attack-object.impl.js'; diff --git a/src/api/common/validated.ts b/src/api/common/validated.ts new file mode 100644 index 00000000..476b4be1 --- /dev/null +++ b/src/api/common/validated.ts @@ -0,0 +1,136 @@ +import { z } from 'zod/v4'; + +type IsPrimitive = T extends object ? false : true; + +export type ValidatedConstructor, WrapValue extends boolean> = { + new ( + value: z.input, + ): Readonly> } : z.infer>; + schema: Schema; + z: >( + this: T, + ) => z.ZodType, z.input>; +}; + +export type ValidatedMutableConstructor< + Schema extends z.ZodType, + WrapValue extends boolean, +> = { + new ( + value: z.input, + ): WrapValue extends true ? { value: z.infer } : z.infer; + schema: Schema; + z: >( + this: T, + ) => z.ZodType, z.input>; +}; + +export const Validated = < + Schema extends z.ZodType, + Options extends { wrapValue: true } | null = null, +>( + schema: Schema, + options?: Options, +) => { + const ctor = function Validated(this: Record, value: z.input) { + const validatedValue = schema.parse(value); + const wrapValue = !isObject(validatedValue) || options?.wrapValue; + const _this = wrapValue ? { value: validatedValue } : validatedValue; + return Object.create(this, Object.getOwnPropertyDescriptors(_this)); + } as unknown as ValidatedConstructor< + Schema, + Options extends { wrapValue: true } ? true : IsPrimitive> + >; + ctor.schema = schema; + ctor.z = function (this: T) { + return z.any().transform((data, ctx) => { + try { + return new this(data) as InstanceType; + } catch (error) { + if (error instanceof z.ZodError) { + for (const issue of error.issues) { + ctx.addIssue(issue); + } + return z.NEVER; + } + throw error; + } + }); + }; + return ctor; +}; + +export const ValidatedMutable = < + Schema extends z.ZodType, + Options extends { wrapValue: true } | null = null, +>( + schema: Schema, + options?: Options, +) => { + const makeValidatedValueProxy = (initialInput: unknown) => { + const inputObject: Record = {}; + if (isObject(initialInput)) { + Object.assign(inputObject, initialInput); + } + return (validatedValue: object) => { + return new Proxy(validatedValue, { + set(object, propertyName, newValue) { + inputObject[propertyName] = newValue; + const validatedNewValue = schema.parse(inputObject) as Record; + return Reflect.set(object, propertyName, validatedNewValue[propertyName]); + }, + }); + }; + }; + const ctor = function ValidatedMutable( + this: Record, + value: z.input, + ) { + const validatedValue = schema.parse(value); + if (!isObject(validatedValue) || options?.wrapValue) { + const validatedValueProxy = isObject(validatedValue) + ? makeValidatedValueProxy(value)(validatedValue) + : validatedValue; + const _this = { value: validatedValueProxy }; + return new Proxy(Object.create(this, Object.getOwnPropertyDescriptors(_this)), { + set(object, propertyName, newValue) { + if (propertyName !== 'value') { + return Reflect.set(object, propertyName, newValue); + } + const validatedNewValue = schema.parse(newValue); + const validatedNewValueProxy = isObject(validatedNewValue) + ? makeValidatedValueProxy(newValue)(validatedNewValue) + : validatedNewValue; + return Reflect.set(object, 'value', validatedNewValueProxy); + }, + }); + } + const _this = validatedValue; + return makeValidatedValueProxy(value)( + Object.create(this, Object.getOwnPropertyDescriptors(_this)), + ); + } as unknown as ValidatedMutableConstructor< + Schema, + Options extends { wrapValue: true } ? true : IsPrimitive> + >; + ctor.schema = schema; + ctor.z = function (this: T) { + return z.any().transform((data, ctx) => { + try { + return new this(data) as InstanceType; + } catch (error) { + if (error instanceof z.ZodError) { + for (const issue of error.issues) { + ctx.addIssue(issue); + } + return z.NEVER; + } + throw error; + } + }); + }; + return ctor; +}; + +const isObject = (value: unknown): value is object => + value !== null && (typeof value === 'object' || typeof value === 'function'); diff --git a/src/classes/index.ts b/src/api/index.ts similarity index 78% rename from src/classes/index.ts rename to src/api/index.ts index 73efc168..2c7a93d9 100644 --- a/src/classes/index.ts +++ b/src/api/index.ts @@ -1,5 +1,5 @@ +export * from '../utils/getters.js'; +export * from './attack-data-model.js'; export * from './common/index.js'; export * from './sdo/index.js'; export * from './smo/index.js'; -export * from './attack-data-model.js'; -export * from './utils.js'; diff --git a/src/classes/sdo/analytic.impl.ts b/src/api/sdo/analytic.impl.ts similarity index 87% rename from src/classes/sdo/analytic.impl.ts rename to src/api/sdo/analytic.impl.ts index 343ebbe2..ba435b1e 100644 --- a/src/classes/sdo/analytic.impl.ts +++ b/src/api/sdo/analytic.impl.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +import { AttackBaseImpl } from '@/api/common/attack-object.impl.js'; import type { Analytic } from '@/schemas/index.js'; -import { AttackBaseImpl } from '@/classes/common/attack-object.impl.js'; export class AnalyticImpl extends AttackBaseImpl implements Analytic { constructor(readonly analytic: Analytic) { diff --git a/src/classes/sdo/asset.impl.ts b/src/api/sdo/asset.impl.ts similarity index 100% rename from src/classes/sdo/asset.impl.ts rename to src/api/sdo/asset.impl.ts diff --git a/src/classes/sdo/campaign.impl.ts b/src/api/sdo/campaign.impl.ts similarity index 100% rename from src/classes/sdo/campaign.impl.ts rename to src/api/sdo/campaign.impl.ts diff --git a/src/classes/sdo/collection.impl.ts b/src/api/sdo/collection.impl.ts similarity index 100% rename from src/classes/sdo/collection.impl.ts rename to src/api/sdo/collection.impl.ts diff --git a/src/classes/sdo/data-component.impl.ts b/src/api/sdo/data-component.impl.ts similarity index 100% rename from src/classes/sdo/data-component.impl.ts rename to src/api/sdo/data-component.impl.ts diff --git a/src/classes/sdo/data-source.impl.ts b/src/api/sdo/data-source.impl.ts similarity index 100% rename from src/classes/sdo/data-source.impl.ts rename to src/api/sdo/data-source.impl.ts diff --git a/src/classes/sdo/detection-strategy.impl.ts b/src/api/sdo/detection-strategy.impl.ts similarity index 93% rename from src/classes/sdo/detection-strategy.impl.ts rename to src/api/sdo/detection-strategy.impl.ts index 500ff705..c8508ca0 100644 --- a/src/classes/sdo/detection-strategy.impl.ts +++ b/src/api/sdo/detection-strategy.impl.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +import { AttackBaseImpl } from '@/api/common/attack-object.impl.js'; import type { DetectionStrategy } from '@/schemas/index.js'; -import { AttackBaseImpl } from '@/classes/common/attack-object.impl.js'; import { TechniqueImpl } from './technique.impl.js'; export class DetectionStrategyImpl extends AttackBaseImpl implements DetectionStrategy { diff --git a/src/classes/sdo/group.impl.ts b/src/api/sdo/group.impl.ts similarity index 100% rename from src/classes/sdo/group.impl.ts rename to src/api/sdo/group.impl.ts diff --git a/src/classes/sdo/identity.impl.ts b/src/api/sdo/identity.impl.ts similarity index 100% rename from src/classes/sdo/identity.impl.ts rename to src/api/sdo/identity.impl.ts diff --git a/src/classes/sdo/index.ts b/src/api/sdo/index.ts similarity index 100% rename from src/classes/sdo/index.ts rename to src/api/sdo/index.ts diff --git a/src/classes/sdo/malware.impl.ts b/src/api/sdo/malware.impl.ts similarity index 100% rename from src/classes/sdo/malware.impl.ts rename to src/api/sdo/malware.impl.ts diff --git a/src/classes/sdo/matrix.impl.ts b/src/api/sdo/matrix.impl.ts similarity index 100% rename from src/classes/sdo/matrix.impl.ts rename to src/api/sdo/matrix.impl.ts diff --git a/src/classes/sdo/mitigation.impl.ts b/src/api/sdo/mitigation.impl.ts similarity index 100% rename from src/classes/sdo/mitigation.impl.ts rename to src/api/sdo/mitigation.impl.ts diff --git a/src/classes/sdo/tactic.impl.ts b/src/api/sdo/tactic.impl.ts similarity index 100% rename from src/classes/sdo/tactic.impl.ts rename to src/api/sdo/tactic.impl.ts diff --git a/src/api/sdo/technique.impl.ts b/src/api/sdo/technique.impl.ts new file mode 100644 index 00000000..1de81707 --- /dev/null +++ b/src/api/sdo/technique.impl.ts @@ -0,0 +1,154 @@ +import type { StixModifiedTimestamp, XMitrePlatform } from '@/schemas/common/index.js'; +import type { AttackObject } from '@/schemas/sdo/index.js'; +import { techniqueSchema } from '@/schemas/sdo/technique.schema.js'; +import { Validated } from '../common/index.js'; +import type { AssetImpl } from './asset.impl.js'; +import type { DataComponentImpl } from './data-component.impl.js'; +import type { MitigationImpl } from './mitigation.impl.js'; +import type { TacticImpl } from './tactic.impl.js'; + +export class TechniqueImpl extends Validated(techniqueSchema) { + // Relationship tracking (mutable, not part of the JSON data) + #subTechniques: TechniqueImpl[] = []; + #tactics: TacticImpl[] = []; + #mitigations: MitigationImpl[] = []; + #parentTechnique?: TechniqueImpl; + #relatedTechniques: TechniqueImpl[] = []; + #targetAssets: AssetImpl[] = []; + #detectingDataComponents: DataComponentImpl[] = []; + #revokedBy?: AttackObject; + + // Relationship management methods + setParent(parent: TechniqueImpl): void { + this.#parentTechnique = parent; + } + + addSubTechnique(subTechnique: TechniqueImpl): void { + if (!this.#subTechniques.some((t) => t.id === subTechnique.id)) { + this.#subTechniques.push(subTechnique); + } + } + + addTactic(tactic: TacticImpl): void { + if (!this.#tactics.some((t) => t.id === tactic.id)) { + this.#tactics.push(tactic); + } + } + + addMitigation(mitigation: MitigationImpl): void { + if (!this.#mitigations.some((m) => m.id === mitigation.id)) { + this.#mitigations.push(mitigation); + } + } + + addRelatedTechnique(technique: TechniqueImpl): void { + if (!this.#relatedTechniques.some((t) => t.id === technique.id)) { + this.#relatedTechniques.push(technique); + } + } + + addTargetAsset(asset: AssetImpl): void { + if (!this.#targetAssets.some((a) => a.id === asset.id)) { + this.#targetAssets.push(asset); + } + } + + addDetectingDataComponent(dataComponent: DataComponentImpl): void { + if (!this.#detectingDataComponents.some((dc) => dc.id === dataComponent.id)) { + this.#detectingDataComponents.push(dataComponent); + } + } + + // Getters for relationships + getSubTechniques(): readonly TechniqueImpl[] { + return [...this.#subTechniques]; + } + + getTactics(): readonly TacticImpl[] { + return [...this.#tactics]; + } + + getMitigations(): readonly MitigationImpl[] { + return [...this.#mitigations]; + } + + getParentTechnique(): TechniqueImpl | undefined { + return this.#parentTechnique; + } + + getRelatedTechniques(): readonly TechniqueImpl[] { + return [...this.#relatedTechniques]; + } + + getTargetAssets(): readonly AssetImpl[] { + return [...this.#targetAssets]; + } + + getDetectingDataComponents(): readonly DataComponentImpl[] { + return [...this.#detectingDataComponents]; + } + + getRevokedBy(): AttackObject | undefined { + return this.#revokedBy; + } + + setRevokedBy(obj: AttackObject | undefined) { + this.#revokedBy = obj; + } + + // Business logic methods + isDeprecated(): boolean { + return this.x_mitre_deprecated ?? false; + } + + isRevoked(): boolean { + return this.revoked ?? false; + } + + isSubTechnique(): boolean { + return this.x_mitre_is_subtechnique ?? false; + } + + getAttackId(): string | undefined { + return this.external_references?.[0]?.external_id; + } + + getDisplayName(): string { + const attackId = this.getAttackId(); + return attackId ? `${attackId}: ${this.name}` : this.name; + } + + getTacticNames(): string[] { + return this.kill_chain_phases?.map((phase) => phase.phase_name) ?? []; + } + + getPlatformsString(): string { + return this.x_mitre_platforms?.join(', ') ?? ''; + } + + supportsPlatform(platform: string): boolean { + return this.x_mitre_platforms?.includes(platform as XMitrePlatform) ?? false; + } + + with(updates: Partial): TechniqueImpl { + const newData = { ...this, ...updates }; + return new TechniqueImpl(newData); + } + + touch(): TechniqueImpl { + return this.with({ + modified: new Date().toISOString() as StixModifiedTimestamp, + }); + } + + equals(other: TechniqueImpl): boolean { + return this.id === other.id && this.modified === other.modified; + } + + isNewerThan(other: TechniqueImpl): boolean { + if (this.id !== other.id) { + throw new Error('Cannot compare different techniques'); + } + return new Date(this.modified) > new Date(other.modified); + } +} diff --git a/src/classes/sdo/tool.impl.ts b/src/api/sdo/tool.impl.ts similarity index 100% rename from src/classes/sdo/tool.impl.ts rename to src/api/sdo/tool.impl.ts diff --git a/src/classes/smo/index.ts b/src/api/smo/index.ts similarity index 100% rename from src/classes/smo/index.ts rename to src/api/smo/index.ts diff --git a/src/classes/smo/marking-definition.impl.ts b/src/api/smo/marking-definition.impl.ts similarity index 100% rename from src/classes/smo/marking-definition.impl.ts rename to src/api/smo/marking-definition.impl.ts diff --git a/src/classes/sro/index.ts b/src/api/sro/index.ts similarity index 100% rename from src/classes/sro/index.ts rename to src/api/sro/index.ts diff --git a/src/classes/sro/relationship.impl.ts b/src/api/sro/relationship.impl.ts similarity index 100% rename from src/classes/sro/relationship.impl.ts rename to src/api/sro/relationship.impl.ts diff --git a/src/classes/sdo/technique.impl.ts b/src/classes/sdo/technique.impl.ts deleted file mode 100644 index eed0ed98..00000000 --- a/src/classes/sdo/technique.impl.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ - -import type { Technique } from '../../schemas/sdo/technique.schema.js'; -import { AttackBaseImpl } from '../common/attack-object.impl.js'; -import { AssetImpl } from './asset.impl.js'; -import { DataComponentImpl } from './data-component.impl.js'; -import { MitigationImpl } from './mitigation.impl.js'; -import { TacticImpl } from './tactic.impl.js'; - -export class TechniqueImpl extends AttackBaseImpl { - private _subTechniques: TechniqueImpl[] = []; - private _tactics: TacticImpl[] = []; - private _mitigations: MitigationImpl[] = []; - private _parentTechnique?: TechniqueImpl; - private _relatedTechniques: TechniqueImpl[] = []; - private _targetAssets: AssetImpl[] = []; - private _detectingDataComponents: DataComponentImpl[] = []; - - constructor(readonly technique: Technique) { - super(); - // Assign properties from the Technique object to this instance - Object.assign(this, technique); - } - - setParent(parent: TechniqueImpl): void { - this._parentTechnique = parent; - } - - addSubTechnique(subTechnique: TechniqueImpl): void { - this._subTechniques.push(subTechnique); - } - - addTactic(tactic: TacticImpl): void { - this._tactics.push(tactic); - } - - addMitigation(mitigation: MitigationImpl): void { - this._mitigations.push(mitigation); - } - - addRelatedTechnique(technique: TechniqueImpl): void { - this._relatedTechniques.push(technique); - } - - addTargetAsset(asset: AssetImpl): void { - this._targetAssets.push(asset); - } - - addDetectingDataComponent(dataComponent: DataComponentImpl): void { - this._detectingDataComponents.push(dataComponent); - } - - // Getters - public getSubTechniques(): TechniqueImpl[] { - return this._subTechniques; - } - - getTactics(): TacticImpl[] { - return this._tactics; - } - - getMitigations(): MitigationImpl[] { - return this._mitigations; - } - - getParentTechnique(): TechniqueImpl | undefined { - return this._parentTechnique; - } - - getRelatedTechniques(): TechniqueImpl[] { - return this._relatedTechniques; - } - - getTargetAssets(): AssetImpl[] { - return this._targetAssets; - } - - getDetectingDataComponents(): DataComponentImpl[] { - return this._detectingDataComponents; - } -} - -// Suppress the lint error for the empty interface -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface TechniqueImpl extends Technique {} - -/* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */ diff --git a/src/data-sources/data-source-registration.ts b/src/data-sources/data-source-registration.ts deleted file mode 100644 index e76a7de3..00000000 --- a/src/data-sources/data-source-registration.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { attackDomainSchema, type AttackDomain } from '../index.js'; -import { fetchAttackVersions } from './fetch-attack-versions.js'; - -export type ParsingMode = 'strict' | 'relaxed'; - -export type DataSourceOptions = - | { - source: 'attack'; - domain: AttackDomain; - version?: string; - parsingMode?: ParsingMode; - } - | { - source: 'file'; - path: string; - parsingMode?: ParsingMode; - } - | { - source: 'url'; - url: string; - parsingMode?: ParsingMode; - } - | { - source: 'taxii'; - url: string; - parsingMode?: ParsingMode; - }; - -/** - * Represents a data source registration with validation logic. - */ -export class DataSourceRegistration { - /** - * Creates a new DataSourceRegistration instance. - * @param options - The data source options to register. - */ - constructor(public readonly options: DataSourceOptions) { - this.validateOptions(); - } - - /** - * Validates the data source options to ensure the correct fields are provided for each source type. - * @throws An error if validation fails. - */ - private async validateOptions(): Promise { - const { source, parsingMode } = this.options; - - // Validate parsing mode - if (parsingMode && !['strict', 'relaxed'].includes(parsingMode)) { - throw new Error(`Invalid parsingMode: ${parsingMode}. Expected 'strict' or 'relaxed'.`); - } - - switch (source) { - case 'attack': { - await this.validateAttackOptions(); - break; - } - case 'file': { - this.validateFileOptions(); - break; - } - case 'url': - case 'taxii': { - throw new Error(`The ${source} source is not implemented yet.`); - } - default: { - throw new Error(`Unsupported data source type: ${source}`); - } - } - } - - /** - * Validates options specific to the 'attack' source type. - * @throws An error if validation fails. - */ - private async validateAttackOptions(): Promise { - const { domain, version } = this.options as { domain: AttackDomain; version?: string }; - - // Validate domain - if (!domain || !Object.values(attackDomainSchema.enum).includes(domain)) { - throw new Error( - `Invalid domain provided for 'attack' source. Expected one of: ${Object.values( - attackDomainSchema.enum, - ).join(', ')}`, - ); - } - - // Validate version if provided - if (version) { - const supportedVersions = await fetchAttackVersions(); - const normalizedVersion = version.replace(/^v/, ''); // Remove leading 'v' if present - if (!supportedVersions.includes(normalizedVersion)) { - throw new Error( - `Invalid version: ${version}. Supported versions are: ${supportedVersions.join(', ')}`, - ); - } - } - } - - /** - * Validates options specific to the 'file' source type. - * @throws An error if validation fails. - */ - private validateFileOptions(): void { - const { path } = this.options as { path: string }; - if (!path) { - throw new Error("The 'file' source requires a 'path' field to specify the file location."); - } - } -} diff --git a/src/data-sources/fetch-attack-versions.ts b/src/data-sources/fetch-attack-versions.ts deleted file mode 100644 index 8e3b7df3..00000000 --- a/src/data-sources/fetch-attack-versions.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Represents a GitHub release object. - */ -interface GitHubRelease { - tag_name: string; - name: string; - published_at: string; -} - -/** - * Normalizes a version string by removing any leading 'v' character. - * @param version - The version string to normalize. - * @returns The normalized version string. - */ -function normalizeVersion(version: string): string { - return version.replace(/^v/, ''); -} - -/** - * Fetches the list of ATT&CK versions from the MITRE ATT&CK STIX data GitHub repository. - * @returns A promise that resolves to an array of version strings. - * @throws An error if the HTTP request fails. - */ -export async function fetchAttackVersions(): Promise { - const url = 'https://api.github.com/repos/mitre-attack/attack-stix-data/releases'; - - // Make a GET request to the GitHub API - const response = await fetch(url, { - headers: { - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }, - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const releases: GitHubRelease[] = await response.json(); - - // Extract and normalize version numbers, then sort them in descending order - const versions = releases - .map((release) => normalizeVersion(release.tag_name)) - .sort((a, b) => { - const [aMajor, aMinor] = a.split('.').map(Number); - const [bMajor, bMinor] = b.split('.').map(Number); - if (bMajor !== aMajor) return bMajor - aMajor; - return bMinor - aMinor; - }); - - return versions; -} diff --git a/src/data-sources/index.ts b/src/data-sources/index.ts deleted file mode 100644 index a7eea2fb..00000000 --- a/src/data-sources/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './data-source-registration.js'; -export * from './fetch-attack-versions.js'; diff --git a/src/index.ts b/src/index.ts index bace3713..c0da346e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,4 @@ -export * from '@/classes/index.js'; -export * from '@/data-sources/index.js'; -export * from '@/schemas/index.js'; -export * from '@/refinements/index.js'; +export * from '@/api/index.js'; export * from '@/main.js'; +export * from '@/schemas/index.js'; +export * from '@/schemas/refinements/index.js'; diff --git a/src/main.ts b/src/main.ts index 0b63856c..036226c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,12 +27,132 @@ import { toolSchema, } from './schemas/index.js'; -import { - DataSourceRegistration, - type ParsingMode, -} from './data-sources/data-source-registration.js'; +import { AttackDataModel } from './api/attack-data-model.js'; +import { attackDomainSchema, type AttackDomain } from './index.js'; + +export type ParsingMode = 'strict' | 'relaxed'; + +export type ContentOriginOptions = + | { + source: 'mitre'; + domain: AttackDomain; + version?: string; + parsingMode?: ParsingMode; + } + | { + source: 'file'; + path: string; + parsingMode?: ParsingMode; + } + | { + source: 'url'; + url: string; + parsingMode?: ParsingMode; + } + | { + source: 'taxii'; + url: string; + parsingMode?: ParsingMode; + }; + +interface GitHubRelease { + tag_name: string; + name: string; + published_at: string; +} + +function normalizeVersion(version: string): string { + return version.replace(/^v/, ''); +} + +export async function fetchAttackVersions(): Promise { + const url = 'https://api.github.com/repos/mitre-attack/attack-stix-data/releases'; + + const response = await fetch(url, { + headers: { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const releases: GitHubRelease[] = await response.json(); + + const versions = releases + .map((release) => normalizeVersion(release.tag_name)) + .sort((a, b) => { + const [aMajor, aMinor] = a.split('.').map(Number); + const [bMajor, bMinor] = b.split('.').map(Number); + if (bMajor !== aMajor) return bMajor - aMajor; + return bMinor - aMinor; + }); + + return versions; +} + +export class ContentOriginRegistration { + constructor(public readonly options: ContentOriginOptions) { + this.validateOptions(); + } + + private async validateOptions(): Promise { + const { source, parsingMode } = this.options; -import { AttackDataModel } from './classes/attack-data-model.js'; + if (parsingMode && !['strict', 'relaxed'].includes(parsingMode)) { + throw new Error(`Invalid parsingMode: ${parsingMode}. Expected 'strict' or 'relaxed'.`); + } + + switch (source) { + case 'mitre': { + await this.validateMitreOptions(); + break; + } + case 'file': { + this.validateFileOptions(); + break; + } + case 'url': + case 'taxii': { + throw new Error(`The ${source} source is not implemented yet.`); + } + default: { + throw new Error(`Unsupported content origin type: ${source}`); + } + } + } + + private async validateMitreOptions(): Promise { + const { domain, version } = this.options as { domain: AttackDomain; version?: string }; + + if (!domain || !Object.values(attackDomainSchema.enum).includes(domain)) { + throw new Error( + `Invalid domain provided for 'mitre' source. Expected one of: ${Object.values( + attackDomainSchema.enum, + ).join(', ')}`, + ); + } + + if (version) { + const supportedVersions = await fetchAttackVersions(); + const normalizedVersion = version.replace(/^v/, ''); + if (!supportedVersions.includes(normalizedVersion)) { + throw new Error( + `Invalid version: ${version}. Supported versions are: ${supportedVersions.join(', ')}`, + ); + } + } + } + + private validateFileOptions(): void { + const { path } = this.options as { path: string }; + if (!path) { + throw new Error("The 'file' source requires a 'path' field to specify the file location."); + } + } +} const readFile = async (path: string): Promise => { if (typeof window !== 'undefined') { @@ -59,31 +179,33 @@ if (typeof window == 'undefined') { 'https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master'; } -interface DataSourceMap { +interface ContentOriginMap { [key: string]: { id: string; model: AttackDataModel; }; } -// Data structure to track registered data sources -const dataSources: DataSourceMap = {}; +// Data structure to track registered content origins +const contentOrigins: ContentOriginMap = {}; /** - * Registers a new data source by fetching and caching ATT&CK data based on the provided options. - * Generates a unique ID for each registered data source. + * Registers a new content origin by fetching and caching ATT&CK data based on the provided options. + * Generates a unique ID for each registered content origin. * - * @param registration - A DataSourceRegistration object containing the source, domain, version, etc. - * @returns The unique ID of the registered data source. + * @param registration - A ContentOriginRegistration object containing the source, domain, version, etc. + * @returns The unique ID of the registered content origin. */ -export async function registerDataSource(registration: DataSourceRegistration): Promise { +export async function registerContentOrigin( + registration: ContentOriginRegistration, +): Promise { const { source, parsingMode = 'strict' } = registration.options; let rawData: StixBundle; - const uniqueId = uuidv4(); // Generate a unique ID for the data source + const uniqueId = uuidv4(); // Generate a unique ID for the content origin switch (source) { - case 'attack': { + case 'mitre': { const { domain, version } = registration.options; rawData = await fetchAttackDataFromGitHub(domain, version); break; @@ -100,7 +222,7 @@ export async function registerDataSource(registration: DataSourceRegistration): break; } default: - throw new Error(`Unsupported source type: ${source}`); + throw new Error(`Unsupported content origin type: ${source}`); } console.log('Retrieved data'); @@ -112,10 +234,10 @@ export async function registerDataSource(registration: DataSourceRegistration): const model = new AttackDataModel(uniqueId, parsedAttackObjects); console.log('Initialized data model.'); - // Store the model and its unique ID in the dataSources map - dataSources[uniqueId] = { id: uniqueId, model }; + // Store the model and its unique ID in the contentOrigins map + contentOrigins[uniqueId] = { id: uniqueId, model }; - return uniqueId; // Return the unique identifier of the data source + return uniqueId; // Return the unique identifier of the content origin } /** @@ -318,15 +440,15 @@ function parseStixBundle(rawData: StixBundle, parsingMode: ParsingMode): AttackO } /** - * Returns the data model of the registered data source, given the data source's unique ID. + * Returns the data model of the registered content origin, given the content origin's unique ID. * * @param id - The unique ID of the data model to retrieve. * @returns The corresponding AttackDataModel instance. */ export function loadDataModel(id: string): AttackDataModel { - const dataSource = dataSources[id]; - if (!dataSource) { - throw new Error(`Data source with ID ${id} not found.`); + const contentOrigin = contentOrigins[id]; + if (!contentOrigin) { + throw new Error(`Content origin with ID ${id} not found.`); } - return dataSource.model; + return contentOrigin.model; } diff --git a/src/refinements/index.ts b/src/schemas/refinements/index.ts similarity index 100% rename from src/refinements/index.ts rename to src/schemas/refinements/index.ts diff --git a/src/schemas/sdo/campaign.schema.ts b/src/schemas/sdo/campaign.schema.ts index 1a0051b2..fd931ef0 100644 --- a/src/schemas/sdo/campaign.schema.ts +++ b/src/schemas/sdo/campaign.schema.ts @@ -1,4 +1,7 @@ -import { createCitationsRefinement, createFirstAliasRefinement } from '@/refinements/index.js'; +import { + createCitationsRefinement, + createFirstAliasRefinement, +} from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { attackBaseDomainObjectSchema } from '../common/attack-base-object.js'; import { diff --git a/src/schemas/sdo/group.schema.ts b/src/schemas/sdo/group.schema.ts index 4d6d04fb..47d3b829 100644 --- a/src/schemas/sdo/group.schema.ts +++ b/src/schemas/sdo/group.schema.ts @@ -1,6 +1,6 @@ -import { createFirstAliasRefinement } from '@/refinements/index.js'; import { attackBaseDomainObjectSchema } from '@/schemas/common/attack-base-object.js'; import { createStixTypeValidator } from '@/schemas/common/stix-type.js'; +import { createFirstAliasRefinement } from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { aliasesSchema, diff --git a/src/schemas/sdo/malware.schema.ts b/src/schemas/sdo/malware.schema.ts index 3fe7d662..6d05a011 100644 --- a/src/schemas/sdo/malware.schema.ts +++ b/src/schemas/sdo/malware.schema.ts @@ -1,7 +1,7 @@ import { createFirstAliasRefinement, createFirstXMitreAliasRefinement, -} from '@/refinements/index.js'; +} from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { createAttackExternalReferencesSchema, diff --git a/src/schemas/sdo/stix-bundle.schema.ts b/src/schemas/sdo/stix-bundle.schema.ts index 567db6c3..91b92b3f 100644 --- a/src/schemas/sdo/stix-bundle.schema.ts +++ b/src/schemas/sdo/stix-bundle.schema.ts @@ -1,12 +1,12 @@ -import { createFirstBundleObjectRefinement } from '@/refinements/index.js'; +import { createFirstBundleObjectRefinement } from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { createStixIdValidator } from '../common/stix-identifier.js'; import { createStixTypeValidator } from '../common/stix-type.js'; import { - markingDefinitionSchema, type MarkingDefinition, + markingDefinitionSchema, } from '../smo/marking-definition.schema.js'; -import { relationshipSchema, type Relationship } from '../sro/relationship.schema.js'; +import { type Relationship, relationshipSchema } from '../sro/relationship.schema.js'; import { type Analytic, analyticSchema } from './analytic.schema.js'; import { type Asset, assetSchema } from './asset.schema.js'; import { type Campaign, campaignSchema } from './campaign.schema.js'; diff --git a/src/schemas/sdo/technique.schema.ts b/src/schemas/sdo/technique.schema.ts index 4d00e635..2dfbd21b 100644 --- a/src/schemas/sdo/technique.schema.ts +++ b/src/schemas/sdo/technique.schema.ts @@ -2,7 +2,7 @@ import { createAttackIdInExternalReferencesRefinement, createEnterpriseOnlyPropertiesRefinement, createMobileOnlyPropertiesRefinement, -} from '@/refinements/index.js'; +} from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { attackBaseDomainObjectSchema, diff --git a/src/schemas/sdo/tool.schema.ts b/src/schemas/sdo/tool.schema.ts index c0e5c143..59bc266a 100644 --- a/src/schemas/sdo/tool.schema.ts +++ b/src/schemas/sdo/tool.schema.ts @@ -1,7 +1,7 @@ import { createFirstAliasRefinement, createFirstXMitreAliasRefinement, -} from '@/refinements/index.js'; +} from '@/schemas/refinements/index.js'; import { z } from 'zod/v4'; import { createAttackExternalReferencesSchema, diff --git a/src/generator/index.ts b/src/utils/generator.ts similarity index 100% rename from src/generator/index.ts rename to src/utils/generator.ts diff --git a/src/classes/utils.ts b/src/utils/getters.ts similarity index 93% rename from src/classes/utils.ts rename to src/utils/getters.ts index 730fe0ce..178f62fa 100644 --- a/src/classes/utils.ts +++ b/src/utils/getters.ts @@ -1,10 +1,10 @@ +import { DataSourceImpl } from '../api/sdo/data-source.impl.js'; +import { MitigationImpl } from '../api/sdo/mitigation.impl.js'; +import { TacticImpl } from '../api/sdo/tactic.impl.js'; import type { XMitrePlatforms } from '../schemas/common/index.js'; import type { DataSource, Mitigation, Tactic, Technique } from '../schemas/sdo/index.js'; import type { AttackObject } from '../schemas/sdo/stix-bundle.schema.js'; import type { Relationship } from '../schemas/sro/relationship.schema.js'; -import { DataSourceImpl } from './sdo/data-source.impl.js'; -import { MitigationImpl } from './sdo/mitigation.impl.js'; -import { TacticImpl } from './sdo/tactic.impl.js'; export function getSubTechniques( technique: Technique, diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 00000000..6c1160a4 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +import { createSyntheticStixObject } from './generator.js'; +import * as getters from './getters.js'; + +export { createSyntheticStixObject, getters }; diff --git a/test/documentation/README.test.ts b/test/documentation/README.test.ts index c991056e..04f3c608 100644 --- a/test/documentation/README.test.ts +++ b/test/documentation/README.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; -import { createSyntheticStixObject } from '../../src/generator/index.js'; +import { createSyntheticStixObject } from '../../src/utils/generator.js'; import { tacticSchema } from '../../src/schemas/index.js'; -import { DataSourceRegistration } from '../../src/data-sources/data-source-registration.js'; -import { AttackDataModel } from '../../src/classes/attack-data-model.js'; +import { ContentOriginRegistration } from '../../src/main.js'; +import { AttackDataModel } from '../../src/api/attack-data-model.js'; describe('README.md Code Examples', () => { describe('Installation Examples', () => { @@ -39,36 +39,36 @@ describe('README.md Code Examples', () => { describe('Recommended Approach Example', () => { it('should work with the loading example from README', () => { // Maps to: README.md - "Recommended Approach" section - // Code block: ```javascript const dataSource = new DataSourceRegistration({ source: 'attack', ... }); - const dataSource = new DataSourceRegistration({ - source: 'attack', + // Code block: ```javascript const contentOrigin = new ContentOriginRegistration({ source: 'mitre', ... }); + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'strict', }); - expect(dataSource.options.source).toBe('attack'); - expect(dataSource.options.domain).toBe('enterprise-attack'); - expect(dataSource.options.version).toBe('17.1'); - expect(dataSource.options.parsingMode).toBe('strict'); + expect(contentOrigin.options.source).toBe('mitre'); + expect((contentOrigin.options as any).domain).toBe('enterprise-attack'); + expect((contentOrigin.options as any).version).toBe('17.1'); + expect(contentOrigin.options.parsingMode).toBe('strict'); }); }); describe('Basic Usage Examples', () => { it('should work with the async function example', () => { // Maps to: README.md - "Basic Usage" section - // Code block: ```typescript const dataSource = new DataSourceRegistration({ ..., parsingMode: 'relaxed' }); - const dataSource = new DataSourceRegistration({ - source: 'attack', + // Code block: ```typescript const contentOrigin = new ContentOriginRegistration({ ..., parsingMode: 'relaxed' }); + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed', }); - expect(dataSource.options.source).toBe('attack'); - expect(dataSource.options.domain).toBe('enterprise-attack'); - expect(dataSource.options.version).toBe('15.1'); - expect(dataSource.options.parsingMode).toBe('relaxed'); + expect(contentOrigin.options.source).toBe('mitre'); + expect((contentOrigin.options as any).domain).toBe('enterprise-attack'); + expect((contentOrigin.options as any).version).toBe('15.1'); + expect(contentOrigin.options.parsingMode).toBe('relaxed'); }); it('should validate that real ATT&CK objects have documented structure', () => { diff --git a/test/documentation/USAGE.test.ts b/test/documentation/USAGE.test.ts index e6acdfe0..580d9c8e 100644 --- a/test/documentation/USAGE.test.ts +++ b/test/documentation/USAGE.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { z } from 'zod'; -import { createSyntheticStixObject } from '../../src/generator/index.js'; +import { createSyntheticStixObject } from '../../src/utils/generator.js'; import { tacticSchema, campaignSchema, techniqueSchema } from '../../src/schemas/index.js'; -import { DataSourceRegistration } from '../../src/data-sources/data-source-registration.js'; -import { AttackDataModel } from '../../src/classes/attack-data-model.js'; +import { ContentOriginRegistration } from '../../src/main.js'; +import { AttackDataModel } from '../../src/api/attack-data-model.js'; describe('docs/USAGE.md Code Examples', () => { describe('Module Format Support Examples', () => { @@ -36,7 +36,7 @@ describe('docs/USAGE.md Code Examples', () => { expect(campaignSchema).toBeDefined(); expect(techniqueSchema).toBeDefined(); expect(AttackDataModel).toBeDefined(); - expect(DataSourceRegistration).toBeDefined(); + expect(ContentOriginRegistration).toBeDefined(); }); }); @@ -140,21 +140,20 @@ describe('docs/USAGE.md Code Examples', () => { }); describe('Initializing with Data Examples', () => { - it('should support DataSource configuration patterns', () => { + it('should support ContentOriginRegistration configuration patterns', () => { // Maps to: docs/USAGE.md - "Initializing with Data" section - // Code block: ```typescript const dataSource = new DataSource({ source: 'attack', ... }); - // Note: Using DataSourceRegistration instead of DataSource as shown in examples - const dataSource = new DataSourceRegistration({ - source: 'attack', + // Code block: ```typescript const contentOrigin = new ContentOriginRegistration({ source: 'mitre', ... }); + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed', }); - expect(dataSource.options.source).toBe('attack'); - expect(dataSource.options.domain).toBe('enterprise-attack'); - expect(dataSource.options.version).toBe('15.1'); - expect(dataSource.options.parsingMode).toBe('relaxed'); + expect(contentOrigin.options.source).toBe('mitre'); + expect((contentOrigin.options as any).domain).toBe('enterprise-attack'); + expect((contentOrigin.options as any).version).toBe('15.1'); + expect(contentOrigin.options.parsingMode).toBe('relaxed'); }); }); @@ -227,19 +226,19 @@ describe('docs/USAGE.md Code Examples', () => { }); }); - describe('Data Sources Examples', () => { - it('should support data source patterns mentioned in USAGE', () => { - // Maps to: docs/USAGE.md - "Data Sources" section + describe('Content Origins Examples', () => { + it('should support content origin patterns mentioned in USAGE', () => { + // Maps to: docs/USAGE.md - "Content Origins" section // Text: "Loading Data from the ATT&CK GitHub Repository" - const dataSource = new DataSourceRegistration({ - source: 'attack', + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '15.1', parsingMode: 'relaxed', }); - expect(dataSource.options.source).toBe('attack'); - expect(dataSource.options.parsingMode).toBe('relaxed'); + expect(contentOrigin.options.source).toBe('mitre'); + expect(contentOrigin.options.parsingMode).toBe('relaxed'); }); }); }); diff --git a/test/documentation/first-query.test.ts b/test/documentation/first-query.test.ts index 178b410f..9607f00e 100644 --- a/test/documentation/first-query.test.ts +++ b/test/documentation/first-query.test.ts @@ -1,23 +1,23 @@ import { describe, it, expect } from 'vitest'; -import { DataSourceRegistration } from '../../src/data-sources/data-source-registration.js'; -import { AttackDataModel } from '../../src/classes/attack-data-model.js'; +import { ContentOriginRegistration } from '../../src/main.js'; +import { AttackDataModel } from '../../src/api/attack-data-model.js'; describe('examples/first-query.ts Code Example', () => { - describe('DataSourceRegistration Configuration', () => { + describe('ContentOriginRegistration Configuration', () => { it('should use the exact configuration from the example', () => { // Maps to: examples/first-query.ts - lines 7-12 - // Code: const dataSource = new DataSourceRegistration({ source: 'attack', domain: 'enterprise-attack', version: '17.1', parsingMode: 'relaxed' }); - const dataSource = new DataSourceRegistration({ - source: 'attack', + // Code: const contentOrigin = new ContentOriginRegistration({ source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'relaxed' }); + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'relaxed', }); - expect(dataSource.options.source).toBe('attack'); - expect(dataSource.options.domain).toBe('enterprise-attack'); - expect(dataSource.options.version).toBe('17.1'); - expect(dataSource.options.parsingMode).toBe('relaxed'); + expect(contentOrigin.options.source).toBe('mitre'); + expect((contentOrigin.options as any).domain).toBe('enterprise-attack'); + expect((contentOrigin.options as any).version).toBe('17.1'); + expect(contentOrigin.options.parsingMode).toBe('relaxed'); }); }); @@ -92,24 +92,24 @@ describe('examples/first-query.ts Code Example', () => { describe('Async Function Pattern Validation', () => { it('should validate the async function structure used in the example', () => { // Maps to: examples/first-query.ts - lines 3-38 - // Code: async function exploreAttackData() { try { const uuid = await registerDataSource(dataSource); } catch (error) { ... } } + // Code: async function exploreAttackData() { try { const uuid = await registerContentOrigin(contentOrigin); } catch (error) { ... } } // Test that the async pattern components work const testAsyncPattern = async () => { - const dataSource = new DataSourceRegistration({ - source: 'attack', + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'relaxed', }); // The example checks these properties exist - expect(dataSource.options.source).toBeDefined(); - expect(dataSource.options.domain).toBeDefined(); - expect(dataSource.options.version).toBeDefined(); - expect(dataSource.options.parsingMode).toBeDefined(); + expect(contentOrigin.options.source).toBeDefined(); + expect((contentOrigin.options as any).domain).toBeDefined(); + expect((contentOrigin.options as any).version).toBeDefined(); + expect(contentOrigin.options.parsingMode).toBeDefined(); - return dataSource; + return contentOrigin; }; expect(testAsyncPattern).not.toThrow(); @@ -165,21 +165,21 @@ describe('examples/first-query.ts Code Example', () => { describe('Import Statement Validation', () => { it('should validate the imports used in the example', () => { // Maps to: examples/first-query.ts - line 1 - // Code: import { registerDataSource, loadDataModel, DataSourceRegistration } from '@mitre-attack/attack-data-model'; + // Code: import { registerContentOrigin, loadDataModel, ContentOriginRegistration } from '@mitre-attack/attack-data-model'; // Test that the imported classes/functions exist and are usable - expect(DataSourceRegistration).toBeDefined(); - expect(typeof DataSourceRegistration).toBe('function'); + expect(ContentOriginRegistration).toBeDefined(); + expect(typeof ContentOriginRegistration).toBe('function'); - // Test that DataSourceRegistration can be instantiated as shown in the example - const dataSource = new DataSourceRegistration({ - source: 'attack', + // Test that ContentOriginRegistration can be instantiated as shown in the example + const contentOrigin = new ContentOriginRegistration({ + source: 'mitre', domain: 'enterprise-attack', version: '17.1', parsingMode: 'relaxed', }); - expect(dataSource).toBeInstanceOf(DataSourceRegistration); + expect(contentOrigin).toBeInstanceOf(ContentOriginRegistration); }); }); }); diff --git a/test/objects/analytic.test.ts b/test/objects/analytic.test.ts index ac81620a..8ed8f770 100644 --- a/test/objects/analytic.test.ts +++ b/test/objects/analytic.test.ts @@ -1,12 +1,12 @@ import { v4 as uuidv4 } from 'uuid'; import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type ExternalReferences } from '../../src/schemas/common/index'; import { type Analytic, analyticSchema, LogSourceReference, } from '../../src/schemas/sdo/analytic.schema'; +import { createSyntheticStixObject } from '../../src/utils/generator'; describe('analyticSchema', () => { const minimalAnalytic = createSyntheticStixObject('x-mitre-analytic'); diff --git a/test/objects/asset.test.ts b/test/objects/asset.test.ts index 738254e1..b40a3ad3 100644 --- a/test/objects/asset.test.ts +++ b/test/objects/asset.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { xMitreIdentity } from '../../src/schemas/common/index'; import { type Asset, assetSchema } from '../../src/schemas/sdo/asset.schema'; +import { createSyntheticStixObject } from '../../src/utils/generator'; /** * Test suite for validating the Asset schema. diff --git a/test/objects/campaign.test.ts b/test/objects/campaign.test.ts index e92391e6..cb483539 100644 --- a/test/objects/campaign.test.ts +++ b/test/objects/campaign.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import type { StixTimestamp } from '../../src/schemas/common/index'; import { type Campaign, campaignSchema } from '../../src/schemas/sdo/campaign.schema'; +import { createSyntheticStixObject } from '../../src/utils/generator'; /** * Test suite for validating the Campaign schema. diff --git a/test/objects/collection.test.ts b/test/objects/collection.test.ts index 632feb17..0eec001a 100644 --- a/test/objects/collection.test.ts +++ b/test/objects/collection.test.ts @@ -1,7 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type Collection, collectionSchema } from '../../src/schemas/sdo/collection.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; /** * Test suite for validating the Collection schema. diff --git a/test/objects/data-component.test.ts b/test/objects/data-component.test.ts index 84f6f305..1fbaff3d 100644 --- a/test/objects/data-component.test.ts +++ b/test/objects/data-component.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type DataComponent, dataComponentSchema, } from '../../src/schemas/sdo/data-component.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; describe('dataComponentSchema', () => { const minimalDataComponent = createSyntheticStixObject('x-mitre-data-component'); diff --git a/test/objects/data-source.test.ts b/test/objects/data-source.test.ts index 07d8177d..0289cbbf 100644 --- a/test/objects/data-source.test.ts +++ b/test/objects/data-source.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type ExternalReferences } from '../../src/schemas/common/index'; import { type DataSource, dataSourceSchema } from '../../src/schemas/sdo/data-source.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; describe('dataSourceSchema', () => { const minimalDataSource = createSyntheticStixObject('x-mitre-data-source'); diff --git a/test/objects/detection-strategy.test.ts b/test/objects/detection-strategy.test.ts index 17b60e19..084b81d6 100644 --- a/test/objects/detection-strategy.test.ts +++ b/test/objects/detection-strategy.test.ts @@ -1,11 +1,11 @@ import { v4 as uuidv4 } from 'uuid'; import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type ExternalReferences } from '../../src/schemas/common/index'; import { - type DetectionStrategy, - detectionStrategySchema, + type DetectionStrategy, + detectionStrategySchema, } from '../../src/schemas/sdo/detection-strategy.schema'; +import { createSyntheticStixObject } from '../../src/utils/generator'; describe('detectionStrategySchema', () => { const minimalDetectionStrategy = createSyntheticStixObject('x-mitre-detection-strategy'); diff --git a/test/objects/group.test.ts b/test/objects/group.test.ts index 2cddc7d2..f219a576 100644 --- a/test/objects/group.test.ts +++ b/test/objects/group.test.ts @@ -1,8 +1,8 @@ import { v4 as uuidv4 } from 'uuid'; import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import type { Description } from '../../src/schemas/common/index'; import { type Group, groupSchema } from '../../src/schemas/sdo/group.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; /** * Test suite for validating the Group schema. diff --git a/test/objects/identity.test.ts b/test/objects/identity.test.ts index 02893418..fcb37f8d 100644 --- a/test/objects/identity.test.ts +++ b/test/objects/identity.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type Identity, identitySchema } from '../../src/schemas/sdo/identity.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; describe('IdentitySchema', () => { const minimalIdentity = createSyntheticStixObject('identity'); diff --git a/test/objects/malware.test.ts b/test/objects/malware.test.ts index d5fbffeb..8896bcc0 100644 --- a/test/objects/malware.test.ts +++ b/test/objects/malware.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type StixTimestamp } from '../../src/schemas/common/index'; import { type Malware, malwareSchema } from '../../src/schemas/sdo/malware.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; describe('MalwareSchema', () => { const minimalMalware = createSyntheticStixObject('malware'); diff --git a/test/objects/marking-definition.test.ts b/test/objects/marking-definition.test.ts index 40002299..eccdf268 100644 --- a/test/objects/marking-definition.test.ts +++ b/test/objects/marking-definition.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { markingDefinitionSchema } from '../../src/schemas/smo/marking-definition.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; /** * Test suite for validating MarkingDefinition schema with "statement" type. diff --git a/test/objects/matrix.test.ts b/test/objects/matrix.test.ts index 94709989..42faa50b 100644 --- a/test/objects/matrix.test.ts +++ b/test/objects/matrix.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type Matrix, matrixSchema } from '../../src/schemas/sdo/matrix.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; /** * Test suite for validating the Matrix schema. diff --git a/test/objects/mitigation.test.ts b/test/objects/mitigation.test.ts index 690785b5..55756757 100644 --- a/test/objects/mitigation.test.ts +++ b/test/objects/mitigation.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type ExternalReferences } from '../../src/schemas/common/index'; import { type Mitigation, mitigationSchema } from '../../src/schemas/sdo/mitigation.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; describe('MitigationSchema', () => { const minimalMitigation = createSyntheticStixObject('course-of-action'); diff --git a/test/objects/relationship.test.ts b/test/objects/relationship.test.ts index 731f379a..56b0201d 100644 --- a/test/objects/relationship.test.ts +++ b/test/objects/relationship.test.ts @@ -1,7 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { z } from 'zod'; -import { createSyntheticStixObject } from '../../src/generator'; import { type Description, type ExternalReferences, @@ -19,6 +18,7 @@ import { type RelationshipType, validRelationshipObjectTypes, } from '../../src/schemas/sro/relationship.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; import { logger } from '../utils/logger'; describe('RelationshipSchema', () => { diff --git a/test/objects/tactic.test.ts b/test/objects/tactic.test.ts index 24751a12..36a7c00a 100644 --- a/test/objects/tactic.test.ts +++ b/test/objects/tactic.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { type Tactic, tacticSchema } from '../../src/schemas/sdo/tactic.schema'; +import { createSyntheticStixObject } from '../../src/utils/index'; /** * Test suite for validating the Tactic schema. diff --git a/test/objects/technique.test.ts b/test/objects/technique.test.ts index fe5a0610..3785416f 100644 --- a/test/objects/technique.test.ts +++ b/test/objects/technique.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; import { xMitreIdentity } from '../../src/schemas/common/index'; import { type Technique, techniqueSchema } from '../../src/schemas/sdo/technique.schema'; +import { createSyntheticStixObject } from '../../src/utils/generator'; /** * Test suite for validating the Technique schema. diff --git a/test/objects/tool.test.ts b/test/objects/tool.test.ts index e33c27f4..b336a233 100644 --- a/test/objects/tool.test.ts +++ b/test/objects/tool.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { createSyntheticStixObject } from '../../src/generator'; +import { createSyntheticStixObject } from '../../src/utils/index'; import { type Tool, toolSchema } from '../../src/schemas/sdo/tool.schema'; /**