Skip to content

Commit 7dd81db

Browse files
committed
Revert "Remove ReadySetCyber (RSC) from TypeScript Backend. (#723)"
This reverts commit fac878c.
1 parent 7c5c248 commit 7dd81db

File tree

15 files changed

+652
-2
lines changed

15 files changed

+652
-2
lines changed

backend/src/api/app.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import * as savedSearches from './saved-searches';
2424
import rateLimit from 'express-rate-limit';
2525
import { createProxyMiddleware } from 'http-proxy-middleware';
2626
import { Organization, User, UserType, connectToDatabase } from '../models';
27+
import * as assessments from './assessments';
2728
import * as jwt from 'jsonwebtoken';
2829
import { Request, Response, NextFunction } from 'express';
2930
import fetch from 'node-fetch';
@@ -123,7 +124,8 @@ app.use(
123124
cors({
124125
origin: [
125126
'http://localhost',
126-
/^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/
127+
/^https:\/\/(.*\.)?crossfeed\.cyber\.dhs\.gov$/,
128+
/^https:\/\/(.*\.)?readysetcyber\.cyber\.dhs\.gov$/
127129
],
128130
methods: 'GET,POST,PUT,DELETE,OPTIONS'
129131
})
@@ -139,7 +141,7 @@ app.use(
139141
`${process.env.COGNITO_URL}`,
140142
`${process.env.BACKEND_DOMAIN}`
141143
],
142-
frameSrc: ["'self'"],
144+
frameSrc: ["'self'", 'https://www.dhs.gov/ntas/'],
143145
imgSrc: [
144146
"'self'",
145147
'data:',
@@ -338,6 +340,7 @@ app.get('/', handlerToExpress(healthcheck));
338340
app.post('/auth/login', handlerToExpress(auth.login));
339341
app.post('/auth/callback', handlerToExpress(auth.callback));
340342
app.post('/users/register', handlerToExpress(users.register));
343+
app.post('/readysetcyber/register', handlerToExpress(users.RSCRegister));
341344

342345
app.get('/notifications', handlerToExpress(notifications.list));
343346
app.get(
@@ -830,6 +833,10 @@ authenticatedRoute.put(
830833
'/notifications/:notificationId',
831834
handlerToExpress(notifications.update)
832835
);
836+
//Authenticated ReadySetCyber Routes
837+
authenticatedRoute.get('/assessments', handlerToExpress(assessments.list));
838+
839+
authenticatedRoute.get('/assessments/:id', handlerToExpress(assessments.get));
833840

834841
//************* */
835842
// V2 Routes //

backend/src/api/assessments.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { validateBody, wrapHandler, NotFound, Unauthorized } from './helpers';
2+
import { Assessment, connectToDatabase } from '../models';
3+
import { isUUID } from 'class-validator';
4+
5+
/**
6+
* @swagger
7+
*
8+
* /assessments:
9+
* post:
10+
* description: Save an RSC assessment to the XFD database.
11+
* tags:
12+
* - Assessments
13+
*/
14+
export const createAssessment = wrapHandler(async (event) => {
15+
const body = await validateBody(Assessment, event.body);
16+
17+
await connectToDatabase();
18+
19+
const assessment = Assessment.create(body);
20+
await Assessment.save(assessment);
21+
22+
return {
23+
statusCode: 200,
24+
body: JSON.stringify(assessment)
25+
};
26+
});
27+
28+
/**
29+
* @swagger
30+
*
31+
* /assessments:
32+
* get:
33+
* description: Lists all assessments for the logged-in user.
34+
* tags:
35+
* - Assessments
36+
*/
37+
export const list = wrapHandler(async (event) => {
38+
const userId = event.requestContext.authorizer!.id;
39+
40+
if (!userId) {
41+
return Unauthorized;
42+
}
43+
44+
await connectToDatabase();
45+
46+
const assessments = await Assessment.find({
47+
where: { user: userId }
48+
});
49+
50+
return {
51+
statusCode: 200,
52+
body: JSON.stringify(assessments)
53+
};
54+
});
55+
56+
/**
57+
* @swagger
58+
*
59+
* /assessments/{id}:
60+
* get:
61+
* description: Return user responses and questions organized by category for a specific assessment.
62+
* parameters:
63+
* - in: path
64+
* name: id
65+
* description: Assessment id
66+
* tags:
67+
* - Assessments
68+
*/
69+
export const get = wrapHandler(async (event) => {
70+
const assessmentId = event.pathParameters?.id;
71+
72+
if (!assessmentId || !isUUID(assessmentId)) {
73+
return NotFound;
74+
}
75+
76+
await connectToDatabase();
77+
78+
const assessment = await Assessment.findOne(assessmentId, {
79+
relations: [
80+
'responses',
81+
'responses.question',
82+
'responses.question.category',
83+
'responses.question.resources'
84+
]
85+
});
86+
87+
if (!assessment) {
88+
return NotFound;
89+
}
90+
91+
// Sort responses by question.number and then by category.number
92+
assessment.responses.sort((a, b) => {
93+
const questionNumberComparison = a.question.number.localeCompare(
94+
b.question.number
95+
);
96+
if (questionNumberComparison !== 0) {
97+
return questionNumberComparison;
98+
} else {
99+
return a.question.category.number.localeCompare(
100+
b.question.category.number
101+
);
102+
}
103+
});
104+
105+
const responsesByCategory = assessment.responses.reduce((acc, response) => {
106+
const categoryName = response.question.category.name;
107+
if (!acc[categoryName]) {
108+
acc[categoryName] = [];
109+
}
110+
acc[categoryName].push(response);
111+
return acc;
112+
}, {});
113+
114+
return {
115+
statusCode: 200,
116+
body: JSON.stringify(responsesByCategory)
117+
};
118+
});

backend/src/api/scans.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ export const SCAN_SCHEMA: ScanSchema = {
165165
description:
166166
'Creates domains from root domains by doing a single DNS lookup for each root domain.'
167167
},
168+
rscSync: {
169+
type: 'fargate',
170+
isPassive: true,
171+
global: true,
172+
description:
173+
'Retrieves and saves assessments from ReadySetCyber mission instance.'
174+
},
168175
savedSearch: {
169176
type: 'fargate',
170177
isPassive: true,

backend/src/api/users.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
isGlobalWriteAdmin,
3737
matchesUserRegion
3838
} from './auth';
39+
import { fetchAssessmentsByUser } from '../tasks/rscSync';
3940

4041
class UserSearch {
4142
@IsInt()
@@ -269,6 +270,30 @@ If you encounter any difficulties, please feel free to reply to this email (or s
269270
);
270271
};
271272

273+
const sendRSCInviteEmail = async (email: string) => {
274+
const staging = process.env.NODE_ENV !== 'production';
275+
276+
await sendEmail(
277+
email,
278+
'ReadySetCyber Dashboard Invitation',
279+
`Hi there,
280+
281+
You've been invited to join ReadySetCyber Dashboard. To accept the invitation and start using your Dashboard, sign on at ${process.env.FRONTEND_DOMAIN}/readysetcyber/create-account.
282+
283+
CyHy Dashboard access instructions:
284+
285+
1. Visit ${process.env.FRONTEND_DOMAIN}/readysetcyber/create-account.
286+
2. Select "Create Account."
287+
3. Enter your email address and a new password for CyHy Dashboard.
288+
4. A confirmation code will be sent to your email. Enter this code when you receive it.
289+
5. You will be prompted to enable MFA. Scan the QR code with an authenticator app on your phone, such as Microsoft Authenticator. Enter the MFA code you see after scanning.
290+
6. After configuring your account, you will be redirected to CyHy Dashboard.
291+
292+
For more information on using CyHy Dashboard, view the CyHy Dashboard user guide at https://docs.crossfeed.cyber.dhs.gov/user-guide/quickstart/.
293+
294+
If you encounter any difficulties, please feel free to reply to this email (or send an email to ${process.env.CROSSFEED_SUPPORT_EMAIL_REPLYTO}).`
295+
);
296+
};
272297
/**
273298
* @swagger
274299
*
@@ -919,3 +944,54 @@ export const updateV2 = wrapHandler(async (event) => {
919944
}
920945
return NotFound;
921946
});
947+
948+
/**
949+
* @swagger
950+
*
951+
* /readysetcyber/register:
952+
* post:
953+
* description: New ReadySetCyber user registration.
954+
* tags:
955+
* - RSCUsers
956+
*/
957+
export const RSCRegister = wrapHandler(async (event) => {
958+
const body = await validateBody(NewUser, event.body);
959+
const newRSCUser = {
960+
firstName: body.firstName,
961+
lastName: body.lastName,
962+
email: body.email.toLowerCase(),
963+
userType: UserType.READY_SET_CYBER
964+
};
965+
966+
await connectToDatabase();
967+
968+
// Check if user already exists
969+
let user = await User.findOne({
970+
email: newRSCUser.email
971+
});
972+
if (user) {
973+
console.log('User already exists.');
974+
return {
975+
statusCode: 422,
976+
body: 'User email already exists. Registration failed.'
977+
};
978+
// Create if user does not exist
979+
} else {
980+
user = User.create(newRSCUser);
981+
await User.save(user);
982+
// Fetch RSC assessments for user
983+
await fetchAssessmentsByUser(user.email);
984+
// Send email notification
985+
if (process.env.IS_LOCAL!) {
986+
console.log('Cannot send invite email while running on local.');
987+
} else {
988+
await sendRSCInviteEmail(user.email);
989+
}
990+
}
991+
992+
const savedUser = await User.findOne(user.id);
993+
return {
994+
statusCode: 200,
995+
body: JSON.stringify(savedUser)
996+
};
997+
});

backend/src/models/assessment.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
BaseEntity,
3+
Column,
4+
Entity,
5+
ManyToOne,
6+
OneToMany,
7+
PrimaryGeneratedColumn
8+
} from 'typeorm';
9+
import { Response } from './response';
10+
import { User } from './user';
11+
12+
@Entity()
13+
export class Assessment extends BaseEntity {
14+
@PrimaryGeneratedColumn('uuid')
15+
id: string;
16+
17+
@Column()
18+
createdAt: Date;
19+
20+
@Column()
21+
updatedAt: Date;
22+
23+
@Column({ unique: true })
24+
rscId: string;
25+
26+
@Column()
27+
type: string;
28+
29+
@ManyToOne(() => User, (user) => user.assessments)
30+
user: User;
31+
32+
@OneToMany(() => Response, (response) => response.assessment)
33+
responses: Response[];
34+
}

backend/src/models/category.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
BaseEntity,
3+
Column,
4+
Entity,
5+
OneToMany,
6+
PrimaryGeneratedColumn
7+
} from 'typeorm';
8+
import { Question } from './question';
9+
10+
@Entity()
11+
export class Category extends BaseEntity {
12+
@PrimaryGeneratedColumn('uuid')
13+
id: string;
14+
15+
@Column()
16+
name: string;
17+
18+
@Column({ unique: true })
19+
number: string;
20+
21+
@Column({ nullable: true })
22+
shortName: string;
23+
24+
@OneToMany(() => Question, (question) => question.category)
25+
questions: Question[];
26+
}

backend/src/models/connection.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import {
33
// Models for the Crossfeed database
44
ApiKey,
55
Notification,
6+
Assessment,
7+
Category,
68
Cpe,
79
Cve,
810
Domain,
911
Organization,
1012
OrganizationTag,
13+
Question,
14+
Resource,
15+
Response,
1116
Role,
1217
SavedSearch,
1318
Scan,
@@ -179,11 +184,16 @@ const connectDb = async (logging?: boolean) => {
179184
database: process.env.DB_NAME,
180185
entities: [
181186
ApiKey,
187+
Assessment,
188+
Category,
182189
Cpe,
183190
Cve,
184191
Domain,
185192
Organization,
186193
OrganizationTag,
194+
Question,
195+
Resource,
196+
Response,
187197
Role,
188198
SavedSearch,
189199
OrganizationTag,

backend/src/models/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export * from './api-key';
2+
export * from './assessment';
3+
export * from './category';
24
export * from './connection';
35
export * from './cpe';
46
export * from './cve';
@@ -7,6 +9,9 @@ export * from './organization';
79
export * from './organization-tag';
810
export * from './material-views';
911
export * from './notification';
12+
export * from './question';
13+
export * from './resource';
14+
export * from './response';
1015
export * from './role';
1116
export * from './saved-search';
1217
export * from './scan';

0 commit comments

Comments
 (0)