From bad3ec89cc8ea298fa3cfe73413e7cb1e2131c0f Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:52:41 -0400 Subject: [PATCH 1/5] fix: change default api root relative path in template.env --- config/template.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/template.env b/config/template.env index 0402f9a..9f1b41f 100644 --- a/config/template.env +++ b/config/template.env @@ -27,7 +27,7 @@ CERTBOT_LE_RSA_KEY_SIZE=4096 # ********************************************************** # ***** CONTACT INFORMATION ******************************** # ********************************************************** -TAXII_API_ROOT_PATH=api +TAXII_API_ROOT_PATH=/api TAXII_API_ROOT_TITLE=ATT&CK Workbench TAXII 2.1 Server TAXII_API_ROOT_DESCRIPTION=Provides access to the latest version of ATT&CK data through a TAXII 2.1 compliant REST API TAXII_CONTACT=your@email.org From e4fdf3125d991c5a13f7b1f6b93d21f8a15b48a9 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:53:54 -0400 Subject: [PATCH 2/5] feat: add snake case interceptor to handle transforming pascalCase properties to snake_case on http responses --- .../interceptors/snake-case.interceptor.ts | 34 +++++++++++++++++++ .../collections/collections.controller.ts | 10 +++--- src/taxii/controllers/root/root.controller.ts | 4 ++- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 src/common/interceptors/snake-case.interceptor.ts diff --git a/src/common/interceptors/snake-case.interceptor.ts b/src/common/interceptors/snake-case.interceptor.ts new file mode 100644 index 0000000..3c5fa56 --- /dev/null +++ b/src/common/interceptors/snake-case.interceptor.ts @@ -0,0 +1,34 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class SnakeCaseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map(data => this.transform(data)) + ); + } + + private transform(data: any) { + if (Array.isArray(data)) { + return data.map(item => this.transformToSnakeCase(item)); + } + return this.transformToSnakeCase(data); + } + + private transformToSnakeCase(data: any) { + if (typeof data !== 'object' || data === null) { + return data; + } + + const snakeCaseData = {}; + for (const key in data) { + if (Object.prototype.hasOwnProperty.call(data, key)) { + const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); + snakeCaseData[snakeKey] = this.transform(data[key]); + } + } + return snakeCaseData; + } +} \ No newline at end of file diff --git a/src/taxii/controllers/collections/collections.controller.ts b/src/taxii/controllers/collections/collections.controller.ts index 63b6f1a..f8ef4a4 100644 --- a/src/taxii/controllers/collections/collections.controller.ts +++ b/src/taxii/controllers/collections/collections.controller.ts @@ -8,9 +8,6 @@ import { UseFilters, UseInterceptors, } from "@nestjs/common"; - -import { ApiExcludeEndpoint, ApiHeader, ApiOkResponse } from "@nestjs/swagger"; - // ** logger ** // import { TaxiiLoggerService as Logger } from "src/common/logger/taxii-logger.service"; @@ -38,6 +35,7 @@ import { TaxiiExceptionFilter } from "src/common/exceptions/taxii-exception.filt import { TimestampQuery } from "src/common/decorators/timestamp.query.decorator"; import { NumberQuery } from "src/common/decorators/number.query.decorator"; import { TaxiiServiceUnavailableException } from "src/common/exceptions"; +import { SnakeCaseInterceptor } from "src/common/interceptors/snake-case.interceptor"; import { SetTaxiiDateHeadersInterceptor, TaxiiDateFrom, @@ -46,8 +44,10 @@ import { // ** transformation pipes ** // import { ParseTimestampPipe } from "src/common/pipes/parse-timestamp.pipe"; import { ParseMatchQueryParamPipe } from "src/common/pipes/parse-match-query-param.pipe"; +import { instanceToPlain } from "class-transformer"; // ** open-api ** // +import { ApiExcludeEndpoint, ApiHeader, ApiOkResponse } from "@nestjs/swagger"; import { SwaggerDocumentation as SWAGGER } from "./collections.controller.swagger.json"; import { VersionsResource } from "../../providers/version/dto/versions-resource"; import { EnvelopeResource } from "src/taxii/providers/envelope/dto/envelope-resource"; @@ -60,6 +60,7 @@ import { ManifestResource } from "../../providers/manifest/dto"; description: SWAGGER.AcceptHeader.Description, }) @Controller("/collections") +@UseInterceptors(SnakeCaseInterceptor) @UseFilters(new TaxiiExceptionFilter()) export class CollectionsController { constructor( @@ -97,7 +98,8 @@ export class CollectionsController { `Received request for a single collection with options { collectionId: ${collectionId} }`, this.constructor.name ); - return await this.collectionService.findOne(collectionId); + const collection = await this.collectionService.findOne(collectionId); + return instanceToPlain(collection, { excludeExtraneousValues: true }) as TaxiiCollectionDto; } @ApiOkResponse({ diff --git a/src/taxii/controllers/root/root.controller.ts b/src/taxii/controllers/root/root.controller.ts index 87a146f..c45bdc5 100644 --- a/src/taxii/controllers/root/root.controller.ts +++ b/src/taxii/controllers/root/root.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param } from "@nestjs/common"; +import { ClassSerializerInterceptor, Controller, Get, Param, UseInterceptors } from "@nestjs/common"; import { TaxiiServiceUnavailableException } from "src/common/exceptions"; import { TaxiiLoggerService as Logger } from "src/common/logger/taxii-logger.service"; import { DiscoveryService } from "src/taxii/providers"; @@ -7,8 +7,10 @@ import { ApiOkResponse } from "@nestjs/swagger"; import { DiscoveryResource } from "../../providers/discovery/dto"; import { SwaggerDocumentation as SWAGGER } from "./root.controller.swagger.json"; import { ApiRootResource } from "../../providers/discovery/dto"; +import { SnakeCaseInterceptor } from "src/common/interceptors/snake-case.interceptor"; @Controller() +@UseInterceptors(SnakeCaseInterceptor) export class RootController { constructor( private readonly discoveryService: DiscoveryService, From 1468feee440d7c25f018ba864884abbdd254bc9a Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:54:54 -0400 Subject: [PATCH 3/5] fix: change 'version' string property on api root dto to 'versions' string array --- .../providers/discovery/dto/api-root.dto.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/taxii/providers/discovery/dto/api-root.dto.ts b/src/taxii/providers/discovery/dto/api-root.dto.ts index 94ea707..c0eed79 100644 --- a/src/taxii/providers/discovery/dto/api-root.dto.ts +++ b/src/taxii/providers/discovery/dto/api-root.dto.ts @@ -1,11 +1,11 @@ -import { IsEnum, IsOptional, IsString } from "class-validator"; +import { IsNumber, IsOptional, IsString } from "class-validator"; import { Exclude, Expose } from "class-transformer"; export interface ApiRootOptions { - title?: string; + title: string; description?: string; - version?: string; - maxContentLength?: number; + versions: string[]; + maxContentLength: number; } @Exclude() @@ -37,8 +37,7 @@ export class ApiRootDto { * @required true */ @Expose() - //@IsEnum(DEFAULT_MEDIA_TYPE) TODO: determine how to validate that string is equal to MediaType.get() - version: string; + versions: string[]; // TODO validate each media type in the list /** * @descr The maximum size of the request body in octets (8-bit bytes) that the server can support. The @@ -51,9 +50,9 @@ export class ApiRootDto { * @type Number (integer) * @required true */ - @Expose() - //@IsEnum(MaxContentLength) - maxContentLength: 1000; + @Expose({ name: 'max_content_length' }) + @IsNumber() + maxContentLength: 1000; // TODO revisit this. 1000 is just a placeholder and has no bearing on server functionality. it is also a static unchangable value. constructor(options: ApiRootOptions) { Object.assign(this, options); From 54a9066f6ad3fc771f3bd6aace30d14092aa4b73 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:55:20 -0400 Subject: [PATCH 4/5] fix: update supported media types --- .../content-negotiation/supported-media-types.ts | 6 ++++-- src/taxii/providers/discovery/discovery.service.ts | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common/middleware/content-negotiation/supported-media-types.ts b/src/common/middleware/content-negotiation/supported-media-types.ts index a1f53fa..dfb7215 100644 --- a/src/common/middleware/content-negotiation/supported-media-types.ts +++ b/src/common/middleware/content-negotiation/supported-media-types.ts @@ -1,12 +1,14 @@ export enum SupportedMediaTypes { - "application" = "application", + Application = "application", } export enum SupportedMediaSubTypes { - "taxii+json" = "taxii+json", + TaxiiJson = "taxii+json", } export enum SupportedMediaVersion { V21 = "2.1", LATEST = "2.1", } + +export const DEFAULT_CONTENT_TYPE = `${SupportedMediaTypes.Application}/${SupportedMediaSubTypes.TaxiiJson};version=${SupportedMediaVersion.LATEST}` as const; diff --git a/src/taxii/providers/discovery/discovery.service.ts b/src/taxii/providers/discovery/discovery.service.ts index f530f52..c1da05d 100644 --- a/src/taxii/providers/discovery/discovery.service.ts +++ b/src/taxii/providers/discovery/discovery.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@nestjs/common"; import { TaxiiConfigService } from "src/config"; import { DiscoverOptions, DiscoveryDto, ApiRootDto } from "./dto"; +import { DEFAULT_CONTENT_TYPE } from "src/common/middleware/content-negotiation/supported-media-types"; @Injectable() export class DiscoveryService { @@ -22,9 +23,9 @@ export class DiscoveryService { return new ApiRootDto({ title: this.config.API_ROOT_TITLE, description: this.config.API_ROOT_DESCRIPTION, - version: "application/taxii+json;version=2.1", // ** A value of "application/taxii+json;version=2.1" MUST - // be included in this list to indicate conformance with - // this specification. ** // + versions: [ + DEFAULT_CONTENT_TYPE, // ** A value of "application/taxii+json;version=2.1" MUST be included in this list to indicate conformance with this specification. ** // + ], maxContentLength: this.config.MAX_CONTENT_LENGTH, }); } From 210189e63e05d752ad2529f62eb9fac2471b5b0a Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:55:46 -0400 Subject: [PATCH 5/5] fix: update default api root path --- src/config/defaults.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 2f12fd5..46446b4 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -2,7 +2,7 @@ export const DEFAULT_ENV = "dev"; export const DEFAULT_APP_ADDRESS = "0.0.0.0"; export const DEFAULT_APP_PORT = 5002; export const DEFAULT_MAX_CONTENT_LENGTH = 1000; -export const DEFAULT_API_ROOT_PATH = "api/v21"; +export const DEFAULT_API_ROOT_PATH = "/api/v21"; export const DEFAULT_API_ROOT_TITLE = "MITRE ATT&CK TAXII 2.1"; export const DEFAULT_API_ROOT_DESCRIPTION = "This API Root contains TAXII 2.1 REST API endpoints that serve MITRE ATT&CK STIX 2.1 data";