Skip to content

Commit

Permalink
Merge pull request #20 from mitre-attack/develop
Browse files Browse the repository at this point in the history
Fix Discovery and Get API root Information endpoints
  • Loading branch information
seansica authored Sep 12, 2024
2 parents 618f493 + 210189e commit f8d4a9f
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 21 deletions.
2 changes: 1 addition & 1 deletion config/template.env
Original file line number Diff line number Diff line change
Expand Up @@ -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=[email protected]
Expand Down
34 changes: 34 additions & 0 deletions src/common/interceptors/snake-case.interceptor.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
10 changes: 6 additions & 4 deletions src/taxii/controllers/collections/collections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand All @@ -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";
Expand All @@ -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(
Expand Down Expand Up @@ -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({
Expand Down
4 changes: 3 additions & 1 deletion src/taxii/controllers/root/root.controller.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/taxii/providers/discovery/discovery.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
});
}
Expand Down
17 changes: 8 additions & 9 deletions src/taxii/providers/discovery/dto/api-root.dto.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down

0 comments on commit f8d4a9f

Please sign in to comment.