diff --git a/backend/src/api/assessments.ts b/backend/src/api/assessments.ts new file mode 100644 index 00000000..56e1f582 --- /dev/null +++ b/backend/src/api/assessments.ts @@ -0,0 +1,103 @@ +import { validateBody, wrapHandler, NotFound, Unauthorized } from './helpers'; +import { Assessment, connectToDatabase } from '../models'; +import { isUUID } from 'class-validator'; + +/** + * @swagger + * + * /assessments: + * post: + * description: Save an RSC assessment to the XFD database. + * tags: + * - Assessments + */ +export const createAssessment = wrapHandler(async (event) => { + const body = await validateBody(Assessment, event.body); + + await connectToDatabase(); + + const assessment = Assessment.create(body); + await Assessment.save(assessment); + + return { + statusCode: 200, + body: JSON.stringify(assessment) + }; +}); + +/** + * @swagger + * + * /assessments: + * get: + * description: Lists all assessments for the logged-in user. + * tags: + * - Assessments + */ +export const list = wrapHandler(async (event) => { + const userId = event.requestContext.authorizer!.id; + + if (!userId) { + return Unauthorized; + } + + await connectToDatabase(); + + const assessments = await Assessment.find({ + where: { user: userId } + }); + + return { + statusCode: 200, + body: JSON.stringify(assessments) + }; +}); + +/** + * @swagger + * + * /assessments/{id}: + * get: + * description: Return user responses and questions organized by category for a specific assessment. + * parameters: + * - in: path + * name: id + * description: Assessment id + * tags: + * - Assessments + */ +export const get = wrapHandler(async (event) => { + const assessmentId = event.pathParameters?.id; + + if (!assessmentId || !isUUID(assessmentId)) { + return NotFound; + } + + await connectToDatabase(); + + const assessment = await Assessment.findOne(assessmentId, { + relations: [ + 'responses', + 'responses.question', + 'responses.question.category' + ] + }); + + if (!assessment) { + return NotFound; + } + + const responsesByCategory = assessment.responses.reduce((acc, response) => { + const categoryName = response.question.category.name; + if (!acc[categoryName]) { + acc[categoryName] = []; + } + acc[categoryName].push(response); + return acc; + }, {}); + + return { + statusCode: 200, + body: JSON.stringify(responsesByCategory) + }; +}); diff --git a/backend/src/models/assessment.ts b/backend/src/models/assessment.ts new file mode 100644 index 00000000..5b792d8d --- /dev/null +++ b/backend/src/models/assessment.ts @@ -0,0 +1,33 @@ +import { + BaseEntity, + Column, + CreateDateColumn, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + UpdateDateColumn +} from 'typeorm'; +import { Response } from './response'; +import { User } from './user'; + +@Entity() +export class Assessment extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @Column() + type: string; + + @ManyToOne(() => User, (user) => user.assessments) + user: User; + + @OneToMany(() => Response, (response) => response.assessment) + responses: Response[]; +} diff --git a/backend/src/models/category.ts b/backend/src/models/category.ts new file mode 100644 index 00000000..17af649a --- /dev/null +++ b/backend/src/models/category.ts @@ -0,0 +1,26 @@ +import { + BaseEntity, + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn +} from 'typeorm'; +import { Question } from './question'; + +@Entity() +export class Category extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column({ nullable: true }) + number: number; + + @Column({ nullable: true }) + shortName: string; + + @OneToMany(() => Question, (question) => question.category) + questions: Question[]; +} diff --git a/backend/src/models/connection.ts b/backend/src/models/connection.ts index dcbb0d67..fb703088 100644 --- a/backend/src/models/connection.ts +++ b/backend/src/models/connection.ts @@ -1,20 +1,25 @@ import { createConnection, Connection } from 'typeorm'; import { // Models for the Crossfeed database + ApiKey, + Assessment, + Category, + Cpe, + Cve, Domain, - Service, - Vulnerability, - Scan, Organization, - User, + OrganizationTag, + Question, + Resource, + Response, Role, + SavedSearch, + Scan, ScanTask, + Service, + User, + Vulnerability, Webpage, - ApiKey, - SavedSearch, - OrganizationTag, - Cpe, - Cve, // Models for the Mini Data Lake database CertScan, @@ -110,20 +115,25 @@ const connectDb = async (logging?: boolean) => { password: process.env.DB_PASSWORD, database: process.env.DB_NAME, entities: [ + ApiKey, + Assessment, + Category, Cpe, Cve, Domain, - Service, - Vulnerability, - Scan, Organization, - User, + OrganizationTag, + Question, + Resource, + Response, Role, - ScanTask, - Webpage, - ApiKey, SavedSearch, - OrganizationTag + Scan, + ScanTask, + Service, + User, + Vulnerability, + Webpage ], synchronize: false, name: 'default', diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 9e1e2145..ab51a506 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,18 +1,23 @@ -export * from './domain'; -export * from './cve'; -export * from './cpe'; -export * from './service'; +export * from './api-key'; +export * from './assessment'; +export * from './category'; export * from './connection'; -export * from './vulnerability'; -export * from './scan'; +export * from './cpe'; +export * from './cve'; +export * from './domain'; export * from './organization'; -export * from './user'; +export * from './organization-tag'; +export * from './question'; +export * from './resource'; +export * from './response'; export * from './role'; +export * from './saved-search'; +export * from './scan'; export * from './scan-task'; +export * from './service'; +export * from './user'; +export * from './vulnerability'; export * from './webpage'; -export * from './api-key'; -export * from './saved-search'; -export * from './organization-tag'; // Mini data lake models export * from './mini_data_lake/cert_scans'; export * from './mini_data_lake/cidrs'; diff --git a/backend/src/models/question.ts b/backend/src/models/question.ts new file mode 100644 index 00000000..dfa3b64c --- /dev/null +++ b/backend/src/models/question.ts @@ -0,0 +1,40 @@ +import { + BaseEntity, + Column, + Entity, + Index, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn +} from 'typeorm'; +import { Category } from './category'; +import { Resource } from './resource'; +import { Response } from './response'; + +@Entity() +@Index(['category', 'number'], { unique: true }) +export class Question extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + longForm: string; + + @Column() + number: number; + + @ManyToMany(() => Resource, (resource) => resource.questions) + @JoinTable() + resources: Resource[]; + + @ManyToOne(() => Category, (category) => category.questions) + category: Category; + + @OneToMany(() => Response, (response) => response.question) + responses: Response[]; +} diff --git a/backend/src/models/resource.ts b/backend/src/models/resource.ts new file mode 100644 index 00000000..3108bb9c --- /dev/null +++ b/backend/src/models/resource.ts @@ -0,0 +1,33 @@ +import { + BaseEntity, + Column, + Entity, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn +} from 'typeorm'; +import { Question } from './question'; + +@Entity() +export class Resource extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + description: string; + + @Column() + name: string; + + @Column('varchar', { array: true }) + possibleResponses: string[]; + + @Column() + type: string; + + @Column() + url: string; + + @ManyToMany(() => Question, (question) => question.resources) + questions: Question[]; +} diff --git a/backend/src/models/response.ts b/backend/src/models/response.ts new file mode 100644 index 00000000..441d4bce --- /dev/null +++ b/backend/src/models/response.ts @@ -0,0 +1,26 @@ +import { + BaseEntity, + Column, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn +} from 'typeorm'; +import { Assessment } from './assessment'; +import { Question } from './question'; + +@Entity() +@Index(['assessment', 'question'], { unique: true }) +export class Response extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + text: string; + + @ManyToOne(() => Assessment, (assessment) => assessment.responses) + assessment: Assessment; + + @ManyToOne(() => Question, (question) => question.responses) + question: Question; +} diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 8a9cdc5f..3d439655 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -1,23 +1,25 @@ import { - Entity, - Index, + BaseEntity, + BeforeInsert, + BeforeUpdate, Column, - UpdateDateColumn, CreateDateColumn, - BaseEntity, + Entity, + Index, OneToMany, - BeforeInsert, PrimaryGeneratedColumn, - BeforeUpdate + UpdateDateColumn } from 'typeorm'; -import { Organization, Role } from './'; import { ApiKey } from './api-key'; +import { Assessment } from './assessment'; +import { Role } from './'; export enum UserType { - STANDARD = 'standard', - GLOBAL_VIEW = 'globalView', GLOBAL_ADMIN = 'globalAdmin', - REGIONAL_ADMIN = 'regionalAdmin' + GLOBAL_VIEW = 'globalView', + REGIONAL_ADMIN = 'regionalAdmin', + READY_SET_CYBER = 'readySetCyber', + STANDARD = 'standard' } @Entity() export class User extends BaseEntity { @@ -116,15 +118,6 @@ export class User extends BaseEntity { }) state: string; - // @Column({ - // nullable: true, - // default: 0 - // }) - // numberOfOrganizations: number; - - // @Column({ - // nullable: true, - // default: [] - // }) - // organizationIds: Array; + @OneToMany(() => Assessment, (assessment) => assessment.user) + assessments: Assessment[]; } diff --git a/backend/src/tasks/rscsyncdb.ts b/backend/src/tasks/rscsyncdb.ts new file mode 100644 index 00000000..e69de29b diff --git a/backend/test/assessments.test.ts b/backend/test/assessments.test.ts new file mode 100644 index 00000000..e69de29b