Kaballas commited on
Commit
56b6519
·
1 Parent(s): 5d43071

initialize project structure with essential configurations and components

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .deepsource.toml +25 -0
  2. .env.example +2 -0
  3. .gitattributes +1 -0
  4. .gitconfig +2 -0
  5. .gitignore +30 -0
  6. LICENSE.md +22 -0
  7. app.py +7 -0
  8. backend/.dockerignore +3 -0
  9. backend/.prettierrc +12 -0
  10. backend/Dockerfile +12 -0
  11. backend/Dockerfile.dev +11 -0
  12. backend/Dockerfile.test +12 -0
  13. backend/README.md +21 -0
  14. backend/babel.config.js +12 -0
  15. backend/docker-compose.dev.yml +41 -0
  16. backend/docker-compose.test.yml +30 -0
  17. backend/jest.config.js +4 -0
  18. backend/report-templates/.gitignore +1 -0
  19. backend/report-templates/.gitkeep +0 -0
  20. backend/src/app.js +170 -0
  21. backend/src/config/config-cwe.json +12 -0
  22. backend/src/config/config.json +33 -0
  23. backend/src/config/roles.json +6 -0
  24. backend/src/lib/auth.js +193 -0
  25. backend/src/lib/custom-generator.js +94 -0
  26. backend/src/lib/cvsscalc31.js +1091 -0
  27. backend/src/lib/html2ooxml.js +204 -0
  28. backend/src/lib/httpResponse.js +82 -0
  29. backend/src/lib/passwordpolicy.js +7 -0
  30. backend/src/lib/report-filters.js +423 -0
  31. backend/src/lib/report-generator.js +707 -0
  32. backend/src/lib/utils.js +58 -0
  33. backend/src/models/audit-type.js +114 -0
  34. backend/src/models/audit.js +1195 -0
  35. backend/src/models/client.js +128 -0
  36. backend/src/models/company.js +95 -0
  37. backend/src/models/custom-field.js +147 -0
  38. backend/src/models/custom-section.js +105 -0
  39. backend/src/models/image.js +78 -0
  40. backend/src/models/language.js +84 -0
  41. backend/src/models/settings.js +188 -0
  42. backend/src/models/template.js +110 -0
  43. backend/src/models/user.js +430 -0
  44. backend/src/models/vulnerability-category.js +124 -0
  45. backend/src/models/vulnerability-type.js +96 -0
  46. backend/src/models/vulnerability-update.js +190 -0
  47. backend/src/models/vulnerability.js +272 -0
  48. backend/src/routes/audit.js +1168 -0
  49. backend/src/routes/check-cwe-update.js +60 -0
  50. backend/src/routes/client.js +80 -0
.deepsource.toml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ xclude_patterns = [
3
+ "dist/**",
4
+ "**/node_modules/",
5
+ "js/**/*.min.js",
6
+ "backend/**/*"
7
+ ]
8
+
9
+ [[analyzers]]
10
+ name = "javascript"
11
+ enabled = true
12
+
13
+ [analyzers.meta]
14
+ environment = ["mongo"]
15
+ plugins = ["react"]
16
+ skip_doc_coverage = ["class-expression", "method-definition"]
17
+ dependency_file_paths = [
18
+ "./frontend/",
19
+ "./"
20
+ ]
21
+ dialect = "typescript"
22
+
23
+ [[transformers]]
24
+ name = "prettier"
25
+ enabled = false
.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ CWE_MODEL_URL=https://drive.usercontent.google.com/download?id=1OtRNObv-Il2B5nDnpzMSGj_yBJAlskuS&export=download&confirm=```
2
+ CVSS_MODEL_URL=https://drive.usercontent.google.com/download?id=1nS1lQpVVJ431wUyVSs5_Srega6QVPyc8&export=download&confirm=
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.gif filter=lfs diff=lfs merge=lfs -text
.gitconfig ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [pull]
2
+ rebase = yes
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ .thumbs.db
3
+ node_modules
4
+ /dist
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+ mongo-data*
9
+ .quasar
10
+ .venv
11
+ package-lock.json
12
+ .env
13
+
14
+ # Editor directories and files
15
+ .idea
16
+ .vscode
17
+ *.suo
18
+ *.ntvs*
19
+ *.njsproj
20
+ *.sln
21
+ .dccache
22
+ .sourcery.yaml
23
+
24
+ # Version managers
25
+ .tool-versions
26
+
27
+
28
+ # modelo
29
+ cwe_api/modelo_cwe
30
+ cwe_api/utils
LICENSE.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Camilo Vera Vidales
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
app.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+
3
+ app = FastAPI()
4
+
5
+ @app.get("/")
6
+ def greet_json():
7
+ return {"Hello": "World!"}
backend/.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules
2
+ mongo-data
3
+ mongo-data-dev
backend/.prettierrc ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "arrowParens": "avoid",
3
+ "singleQuote": true,
4
+ "overrides": [
5
+ {
6
+ "files": ".changeset/**/*",
7
+ "options": {
8
+ "singleQuote": false
9
+ }
10
+ }
11
+ ]
12
+ }
backend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine
2
+
3
+ RUN mkdir -p /app
4
+ WORKDIR /app
5
+ COPY package*.json ./
6
+ RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
7
+ RUN npm install
8
+ COPY . .
9
+ EXPOSE 4242
10
+ ENV NODE_ENV prod
11
+ ENV NODE_ICU_DATA=node_modules/full-icu
12
+ ENTRYPOINT ["npm", "start"]
backend/Dockerfile.dev ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine
2
+
3
+ RUN mkdir -p /app
4
+ WORKDIR /app
5
+ COPY package*.json ./
6
+ RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
7
+ RUN npm install
8
+ COPY . .
9
+ ENV NODE_ENV dev
10
+ ENV NODE_ICU_DATA=node_modules/full-icu
11
+ ENTRYPOINT [ "npm", "run", "dev"]
backend/Dockerfile.test ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:lts-alpine
2
+
3
+ RUN mkdir -p /app
4
+ WORKDIR /app
5
+ COPY package*.json ./
6
+ RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
7
+ RUN npm install
8
+ COPY . .
9
+ ENV NODE_ENV test
10
+ ENV NODE_ICU_DATA=node_modules/full-icu
11
+ RUN npm install
12
+ ENTRYPOINT ["npm", "run", "test"]
backend/README.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Installation for developpment environnment
2
+
3
+ *Source code can be modified live and application will automatically reload on changes.*
4
+
5
+ Build and run Docker containers
6
+ ```
7
+ docker-compose -f ./docker-compose.dev.yml up -d --build
8
+ ```
9
+
10
+ Display container logs
11
+ ```
12
+ docker-compose logs -f
13
+ ```
14
+
15
+ Stop/Start container
16
+ ```
17
+ docker-compose stop
18
+ docker-compose start
19
+ ```
20
+
21
+ API is accessible through https://localhost:5252/api
backend/babel.config.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ presets: [
3
+ [
4
+ '@babel/preset-env',
5
+ {
6
+ targets: {
7
+ node: 'current',
8
+ },
9
+ },
10
+ ],
11
+ ],
12
+ };
backend/docker-compose.dev.yml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+ services:
3
+ mongodb:
4
+ image: mongo:4.2.15
5
+ container_name: mongo-auditforge-dev
6
+ volumes:
7
+ - ./mongo-data-dev:/data/db
8
+ restart: always
9
+ ports:
10
+ - 127.0.0.1:27017:27017
11
+ environment:
12
+ - MONGO_DB:auditforge
13
+ networks:
14
+ - backend
15
+
16
+ auditforge-backend:
17
+ build:
18
+ context: .
19
+ dockerfile: Dockerfile.dev
20
+ image: auditforge:backend-dev
21
+ container_name: auditforge-backend-dev
22
+ volumes:
23
+ - ./src:/app/src
24
+ - ./ssl:/app/ssl
25
+ - ./report-templates:/app/report-templates
26
+ depends_on:
27
+ - mongodb
28
+ restart: always
29
+ ports:
30
+ - 5252:5252
31
+ links:
32
+ - mongodb
33
+ networks:
34
+ - backend
35
+
36
+ volumes:
37
+ mongo-data-dev:
38
+
39
+ networks:
40
+ backend:
41
+ driver: bridge
backend/docker-compose.test.yml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+ services:
3
+ mongodb-test:
4
+ image: mongo:4.2.15
5
+ container_name: mongo-auditforge-test
6
+ volumes:
7
+ - ./mongo-data-test:/data/db
8
+ restart: always
9
+ environment:
10
+ - MONGO_DB:auditforge
11
+ network_mode: host
12
+
13
+ backend-test:
14
+ image: auditforge:backend-test
15
+ build:
16
+ context: .
17
+ dockerfile: Dockerfile.test
18
+ container_name: auditforge-backend-test
19
+ volumes:
20
+ - ./tests:/app/tests
21
+ - ./src:/app/src
22
+ - ./jest.config.js:/app/jest.config.js
23
+ environment:
24
+ API_URL: https://localhost:4242
25
+ depends_on:
26
+ - mongodb-test
27
+ network_mode: host
28
+
29
+ volumes:
30
+ mongo-data-test:
backend/jest.config.js ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ verbose: true,
4
+ };
backend/report-templates/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ *.docx
backend/report-templates/.gitkeep ADDED
File without changes
backend/src/app.js ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var fs = require('fs');
2
+ var app = require('express')();
3
+
4
+ var https = require('https').Server(
5
+ {
6
+ key: fs.readFileSync(__dirname + '/../ssl/server.key'),
7
+ cert: fs.readFileSync(__dirname + '/../ssl/server.cert'),
8
+
9
+ // TLS Versions
10
+ maxVersion: 'TLSv1.3',
11
+ minVersion: 'TLSv1.2',
12
+
13
+ // Hardened configuration
14
+ ciphers:
15
+ 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384',
16
+
17
+ honorCipherOrder: false,
18
+ },
19
+ app,
20
+ );
21
+ app.disable('x-powered-by');
22
+
23
+ var io = require('socket.io')(https, {
24
+ cors: {
25
+ origin: '*',
26
+ },
27
+ });
28
+ var bodyParser = require('body-parser');
29
+ var cookieParser = require('cookie-parser');
30
+ var utils = require('./lib/utils');
31
+
32
+ // Get configuration
33
+ var env = process.env.NODE_ENV || 'dev';
34
+ var config = require('./config/config.json')[env];
35
+ global.__basedir = __dirname;
36
+
37
+ // Database connection
38
+ var mongoose = require('mongoose');
39
+ // Use native promises
40
+ mongoose.Promise = global.Promise;
41
+ // Trim all Strings
42
+ mongoose.Schema.Types.String.set('trim', true);
43
+
44
+ mongoose.connect(
45
+ `mongodb://${config.database.server}:${config.database.port}/${config.database.name}`,
46
+ {},
47
+ );
48
+
49
+ // Models import
50
+ require('./models/user');
51
+ require('./models/audit');
52
+ require('./models/client');
53
+ require('./models/company');
54
+ require('./models/template');
55
+ require('./models/vulnerability');
56
+ require('./models/vulnerability-update');
57
+ require('./models/language');
58
+ require('./models/audit-type');
59
+ require('./models/vulnerability-type');
60
+ require('./models/vulnerability-category');
61
+ require('./models/custom-section');
62
+ require('./models/custom-field');
63
+ require('./models/image');
64
+ require('./models/settings');
65
+
66
+ // Socket IO configuration
67
+ io.on('connection', socket => {
68
+ socket.on('join', data => {
69
+ console.log(
70
+ `user ${data.username.replace(/\n|\r/g, '')} joined room ${data.room.replace(/\n|\r/g, '')}`,
71
+ );
72
+ socket.username = data.username;
73
+ do {
74
+ socket.color =
75
+ '#' + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
76
+ } while (socket.color === '#77c84e');
77
+ socket.join(data.room);
78
+ io.to(data.room).emit('updateUsers');
79
+ });
80
+ socket.on('leave', data => {
81
+ console.log(
82
+ `user ${data.username.replace(/\n|\r/g, '')} left room ${data.room.replace(/\n|\r/g, '')}`,
83
+ );
84
+ socket.leave(data.room);
85
+ io.to(data.room).emit('updateUsers');
86
+ });
87
+ socket.on('updateUsers', data => {
88
+ var userList = [
89
+ ...new Set(
90
+ utils.getSockets(io, data.room).map(s => {
91
+ var user = {};
92
+ user.username = s.username;
93
+ user.color = s.color;
94
+ user.menu = s.menu;
95
+ if (s.finding) user.finding = s.finding;
96
+ if (s.section) user.section = s.section;
97
+ return user;
98
+ }),
99
+ ),
100
+ ];
101
+ io.to(data.room).emit('roomUsers', userList);
102
+ });
103
+ socket.on('menu', data => {
104
+ socket.menu = data.menu;
105
+ data.finding ? (socket.finding = data.finding) : delete socket.finding;
106
+ data.section ? (socket.section = data.section) : delete socket.section;
107
+ io.to(data.room).emit('updateUsers');
108
+ });
109
+ socket.on('disconnect', () => {
110
+ socket.broadcast.emit('updateUsers');
111
+ });
112
+ });
113
+
114
+ // CORS
115
+ app.use(function (req, res, next) {
116
+ res.header('Access-Control-Allow-Origin', req.headers.origin);
117
+ res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE,PUT,OPTIONS');
118
+ res.header(
119
+ 'Access-Control-Allow-Headers',
120
+ 'Origin, X-Requested-With, Content-Type, Accept',
121
+ );
122
+ res.header('Access-Control-Expose-Headers', 'Content-Disposition');
123
+ res.header('Access-Control-Allow-Credentials', 'true');
124
+ next();
125
+ });
126
+
127
+ // CSP
128
+ app.use(function (req, res, next) {
129
+ res.header(
130
+ 'Content-Security-Policy',
131
+ "default-src 'none'; form-action 'none'; base-uri 'self'; frame-ancestors 'none'; sandbox; require-trusted-types-for 'script';",
132
+ );
133
+ next();
134
+ });
135
+
136
+ app.use(bodyParser.json({ limit: '100mb' }));
137
+ app.use(
138
+ bodyParser.urlencoded({
139
+ limit: '10mb',
140
+ extended: false, // do not need to take care about images, videos -> false: only strings
141
+ }),
142
+ );
143
+
144
+ app.use(cookieParser());
145
+
146
+ // Routes import
147
+ require('./routes/user')(app);
148
+ require('./routes/audit')(app, io);
149
+ require('./routes/client')(app);
150
+ require('./routes/company')(app);
151
+ require('./routes/vulnerability')(app);
152
+ require('./routes/template')(app);
153
+ require('./routes/vulnerability')(app);
154
+ require('./routes/data')(app);
155
+ require('./routes/image')(app);
156
+ require('./routes/settings')(app);
157
+ require('./routes/cwe')(app);
158
+ require('./routes/cvss')(app);
159
+ require('./routes/check-cwe-update')(app);
160
+ require('./routes/update-cwe-model')(app);
161
+
162
+ app.get('*', function (req, res) {
163
+ res.status(404).json({ status: 'error', data: 'Route undefined' });
164
+ });
165
+
166
+ // Start server
167
+
168
+ https.listen(config.port, config.host);
169
+
170
+ module.exports = app;
backend/src/config/config-cwe.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cwe-container": {
3
+ "host": "auditforge-cwe-api",
4
+ "port": 8000,
5
+ "check_timeout_ms": 30000,
6
+ "update_timeout_ms": 120000,
7
+ "endpoints": {
8
+ "check_update_endpoint": "check_cwe_update",
9
+ "update_cwe_endpoint": "update_cwe_model"
10
+ }
11
+ }
12
+ }
backend/src/config/config.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dev": {
3
+ "port": 5252,
4
+ "host": "",
5
+ "database": {
6
+ "name": "auditforge",
7
+ "server": "mongo-auditforge-dev",
8
+ "port": "27017"
9
+ },
10
+ "jwtSecret": "1a039133523dfca9cd5d0ac385c16f0f061558a96dc57384854ef90ed53e86dd",
11
+ "jwtRefreshSecret": "95b0c96984c301d94b41c3d2306fd041a001c58516eb5a23f83044822f42e558"
12
+ },
13
+ "prod": {
14
+ "port": 4242,
15
+ "host": "",
16
+ "database": {
17
+ "name": "auditforge",
18
+ "server": "mongo-auditforge",
19
+ "port": "27017"
20
+ },
21
+ "jwtSecret": "8565a16daedc531581393a08812af3c9043e702069216c54bd51c6613bcf9811",
22
+ "jwtRefreshSecret": "54f27d78990b5f4537702dbf97d9d746c7cb8172f070a1212933c877e8fc98a8"
23
+ },
24
+ "test": {
25
+ "port": 6262,
26
+ "host": "",
27
+ "database": {
28
+ "name": "auditforge",
29
+ "server": "127.0.0.1",
30
+ "port": "27017"
31
+ }
32
+ }
33
+ }
backend/src/config/roles.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "report": {
3
+ "inherits": ["user"],
4
+ "allows": ["audits:read-all"]
5
+ }
6
+ }
backend/src/lib/auth.js ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Dynamic generation of JWT Secret if not exist (different for each environnment)
2
+ var fs = require('fs');
3
+ var env = process.env.NODE_ENV || 'dev';
4
+ var config = require('../config/config.json');
5
+
6
+ if (!config[env].jwtSecret) {
7
+ config[env].jwtSecret = require('crypto').randomBytes(32).toString('hex');
8
+ var configString = JSON.stringify(config, null, 4);
9
+ fs.writeFileSync(`${__basedir}/config/config.json`, configString);
10
+ }
11
+ if (!config[env].jwtRefreshSecret) {
12
+ config[env].jwtRefreshSecret = require('crypto')
13
+ .randomBytes(32)
14
+ .toString('hex');
15
+ var configString = JSON.stringify(config, null, 4);
16
+ fs.writeFileSync(`${__basedir}/config/config.json`, configString);
17
+ }
18
+
19
+ var jwtSecret = config[env].jwtSecret;
20
+ exports.jwtSecret = jwtSecret;
21
+
22
+ var jwtRefreshSecret = config[env].jwtRefreshSecret;
23
+ exports.jwtRefreshSecret = jwtRefreshSecret;
24
+
25
+ /* ROLES LOGIC
26
+
27
+ role_name: {
28
+ allows: [],
29
+ inherits: []
30
+ }
31
+ allows: allowed permissions to access | use * for all
32
+ inherits: inherits other users "allows"
33
+ */
34
+
35
+ const builtInRoles = {
36
+ user: {
37
+ allows: [
38
+ // Audits
39
+ 'audits:create',
40
+ 'audits:read',
41
+ 'audits:update',
42
+ 'audits:delete',
43
+ // Images
44
+ 'images:create',
45
+ 'images:read',
46
+ // Clients
47
+ 'clients:create',
48
+ 'clients:read',
49
+ 'clients:update',
50
+ 'clients:delete',
51
+ // Companies
52
+ 'companies:create',
53
+ 'companies:read',
54
+ 'companies:update',
55
+ 'companies:delete',
56
+ // Languages
57
+ 'languages:read',
58
+ // Audit Types
59
+ 'audit-types:read',
60
+ // Vulnerability Types
61
+ 'vulnerability-types:read',
62
+ // Vulnerability Categories
63
+ 'vulnerability-categories:read',
64
+ // Sections Data
65
+ 'sections:read',
66
+ // Templates
67
+ 'templates:read',
68
+ // Users
69
+ 'users:read',
70
+ // Roles
71
+ 'roles:read',
72
+ // Vulnerabilities
73
+ 'vulnerabilities:read',
74
+ 'vulnerability-updates:create',
75
+ // Custom Fields
76
+ 'custom-fields:read',
77
+ // Settings
78
+ 'settings:read-public',
79
+ // Classify
80
+ 'classify:all',
81
+ // Check CWE Update
82
+ 'check-update:all',
83
+ // Update CWE Model
84
+ 'update-model:all',
85
+ ],
86
+ },
87
+ admin: {
88
+ allows: '*',
89
+ },
90
+ };
91
+
92
+ try {
93
+ var customRoles = require('../config/roles.json');
94
+ } catch (error) {
95
+ var customRoles = [];
96
+ }
97
+ var roles = { ...customRoles, ...builtInRoles };
98
+
99
+ class ACL {
100
+ constructor(roles) {
101
+ if (typeof roles !== 'object') {
102
+ throw new TypeError('Expected an object as input');
103
+ }
104
+ this.roles = roles;
105
+ }
106
+
107
+ isAllowed(role, permission) {
108
+ // Check if role exists
109
+ if (!this.roles[role] && !this.roles['user']) {
110
+ return false;
111
+ }
112
+
113
+ let $role = this.roles[role] || this.roles['user']; // Default to user role in case of inexistant role
114
+ // Check if role is allowed with permission
115
+ if (
116
+ $role.allows &&
117
+ ($role.allows === '*' || $role.allows.indexOf(permission) !== -1)
118
+ ) {
119
+ return true;
120
+ }
121
+
122
+ // Check if there is inheritance
123
+ if (!$role.inherits || $role.inherits.length < 1) {
124
+ return false;
125
+ }
126
+
127
+ // Recursive check childs until true or false
128
+ return $role.inherits.some(role => this.isAllowed(role, permission));
129
+ }
130
+
131
+ hasPermission(permission) {
132
+ var Response = require('./httpResponse');
133
+ var jwt = require('jsonwebtoken');
134
+
135
+ return (req, res, next) => {
136
+ if (!req.cookies['token']) {
137
+ Response.Unauthorized(res, 'No token provided');
138
+ return;
139
+ }
140
+
141
+ var cookie = req.cookies['token'].split(' ');
142
+ if (cookie.length !== 2 || cookie[0] !== 'JWT') {
143
+ Response.Unauthorized(res, 'Bad token type');
144
+ return;
145
+ }
146
+
147
+ var token = cookie[1];
148
+ jwt.verify(token, jwtSecret, (err, decoded) => {
149
+ if (err) {
150
+ if (err.name === 'TokenExpiredError')
151
+ Response.Unauthorized(res, 'Expired token');
152
+ else Response.Unauthorized(res, 'Invalid token');
153
+ return;
154
+ }
155
+
156
+ if (
157
+ permission === 'validtoken' ||
158
+ this.isAllowed(decoded.role, permission)
159
+ ) {
160
+ req.decodedToken = decoded;
161
+ return next();
162
+ } else {
163
+ Response.Forbidden(res, 'Insufficient privileges');
164
+ return;
165
+ }
166
+ });
167
+ };
168
+ }
169
+
170
+ buildRoles(role) {
171
+ var currentRole = this.roles[role] || this.roles['user']; // Default to user role in case of inexistant role
172
+
173
+ var result = currentRole.allows || [];
174
+
175
+ if (currentRole.inherits) {
176
+ currentRole.inherits.forEach(element => {
177
+ result = [...new Set([...result, ...this.buildRoles(element)])];
178
+ });
179
+ }
180
+
181
+ return result;
182
+ }
183
+
184
+ getRoles(role) {
185
+ var result = this.buildRoles(role);
186
+
187
+ if (result.includes('*')) return '*';
188
+
189
+ return result;
190
+ }
191
+ }
192
+
193
+ exports.acl = new ACL(roles);
backend/src/lib/custom-generator.js ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var expressions = require('angular-expressions');
2
+
3
+ // Apply all customs functions
4
+ function apply(data) {}
5
+ exports.apply = apply;
6
+
7
+ // *** Custom modifications of audit data for usage in word template
8
+
9
+ // *** Custome Angular expressions filters ***
10
+
11
+ var filters = {};
12
+
13
+ // Convert input CVSS criteria into French: {input | criteriaFR}
14
+ expressions.filters.criteriaFR = function (input) {
15
+ var result = 'Non défini';
16
+
17
+ if (input === 'Network') result = 'Réseau';
18
+ else if (input === 'Adjacent Network') result = 'Réseau Local';
19
+ else if (input === 'Local') result = 'Local';
20
+ else if (input === 'Physical') result = 'Physique';
21
+ else if (input === 'None') result = 'Aucun';
22
+ else if (input === 'Low') result = 'Faible';
23
+ else if (input === 'High') result = 'Haute';
24
+ else if (input === 'Required') result = 'Requis';
25
+ else if (input === 'Unchanged') result = 'Inchangé';
26
+ else if (input === 'Changed') result = 'Changé';
27
+
28
+ return result;
29
+ };
30
+
31
+ // Convert input date with parameter s (full,short): {input | convertDate: 's'}
32
+ expressions.filters.convertDateFR = function (input, s) {
33
+ var date = new Date(input);
34
+ if (date !== 'Invalid Date') {
35
+ var monthsFull = [
36
+ 'Janvier',
37
+ 'Février',
38
+ 'Mars',
39
+ 'Avril',
40
+ 'Mai',
41
+ 'Juin',
42
+ 'Juillet',
43
+ 'Août',
44
+ 'Septembre',
45
+ 'Octobre',
46
+ 'Novembre',
47
+ 'Décembre',
48
+ ];
49
+ var monthsShort = [
50
+ '01',
51
+ '02',
52
+ '03',
53
+ '04',
54
+ '05',
55
+ '06',
56
+ '07',
57
+ '08',
58
+ '09',
59
+ '10',
60
+ '11',
61
+ '12',
62
+ ];
63
+ var days = [
64
+ 'Dimanche',
65
+ 'Lundi',
66
+ 'Mardi',
67
+ 'Mercredi',
68
+ 'Jeudi',
69
+ 'Vendredi',
70
+ 'Samedi',
71
+ ];
72
+ var day = date.getUTCDate();
73
+ var month = date.getUTCMonth();
74
+ var year = date.getUTCFullYear();
75
+ if (s === 'full') {
76
+ return (
77
+ days[date.getUTCDay()] +
78
+ ' ' +
79
+ (day < 10 ? '0' + day : day) +
80
+ ' ' +
81
+ monthsFull[month] +
82
+ ' ' +
83
+ year
84
+ );
85
+ }
86
+ if (s === 'short') {
87
+ return (
88
+ (day < 10 ? '0' + day : day) + '/' + monthsShort[month] + '/' + year
89
+ );
90
+ }
91
+ }
92
+ };
93
+
94
+ exports.expressions = expressions;
backend/src/lib/cvsscalc31.js ADDED
@@ -0,0 +1,1091 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Copyright (c) 2019, FIRST.ORG, INC.
2
+ * All rights reserved.
3
+ *
4
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
5
+ * following conditions are met:
6
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
7
+ * disclaimer.
8
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
9
+ * following disclaimer in the documentation and/or other materials provided with the distribution.
10
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
11
+ * products derived from this software without specific prior written permission.
12
+ *
13
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
14
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
16
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
17
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
18
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
19
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
20
+ */
21
+
22
+ /* This JavaScript contains two main functions. Both take CVSS metric values and calculate CVSS scores for Base,
23
+ * Temporal and Environmental metric groups, their associated severity ratings, and an overall Vector String.
24
+ *
25
+ * Use CVSS31.calculateCVSSFromMetrics if you wish to pass metric values as individual parameters.
26
+ * Use CVSS31.calculateCVSSFromVector if you wish to pass metric values as a single Vector String.
27
+ *
28
+ * Changelog
29
+ *
30
+ * 2019-06-01 Darius Wiles Updates for CVSS version 3.1:
31
+ *
32
+ * 1) The CVSS31.roundUp1 function now performs rounding using integer arithmetic to
33
+ * eliminate problems caused by tiny errors introduced during JavaScript math
34
+ * operations. Thanks to Stanislav Kontar of Red Hat for suggesting and testing
35
+ * various implementations.
36
+ *
37
+ * 2) Environmental formulas changed to prevent the Environmental Score decreasing when
38
+ * the value of an Environmental metric is raised. The problem affected a small
39
+ * percentage of CVSS v3.0 metrics. The change is to the modifiedImpact
40
+ * formula, but only affects scores where the Modified Scope is Changed (or the
41
+ * Scope is Changed if Modified Scope is Not Defined).
42
+ *
43
+ * 3) The JavaScript object containing everything in this file has been renamed from
44
+ * "CVSS" to "CVSS31" to allow both objects to be included without causing a
45
+ * naming conflict.
46
+ *
47
+ * 4) Variable names and code order have changed to more closely reflect the formulas
48
+ * in the CVSS v3.1 Specification Document.
49
+ *
50
+ * 5) A successful call to calculateCVSSFromMetrics now returns sub-formula values.
51
+ *
52
+ * Note that some sets of metrics will produce different scores between CVSS v3.0 and
53
+ * v3.1 as a result of changes 1 and 2. See the explanation of changes between these
54
+ * two standards in the CVSS v3.1 User Guide for more details.
55
+ *
56
+ * 2018-02-15 Darius Wiles Added a missing pair of parentheses in the Environmental score, specifically
57
+ * in the code setting envScore in the main clause (not the else clause). It was changed
58
+ * from "min (...), 10" to "min ((...), 10)". This correction does not alter any final
59
+ * Environmental scores.
60
+ *
61
+ * 2015-08-04 Darius Wiles Added CVSS.generateXMLFromMetrics and CVSS.generateXMLFromVector functions to return
62
+ * XML string representations of: a set of metric values; or a Vector String respectively.
63
+ * Moved all constants and functions to an object named "CVSS" to
64
+ * reduce the chance of conflicts in global variables when this file is combined with
65
+ * other JavaScript code. This will break all existing code that uses this file until
66
+ * the string "CVSS." is prepended to all references. The "Exploitability" metric has been
67
+ * renamed "Exploit Code Maturity" in the specification, so the same change has been made
68
+ * in the code in this file.
69
+ *
70
+ * 2015-04-24 Darius Wiles Environmental formula modified to eliminate undesirable behavior caused by subtle
71
+ * differences in rounding between Temporal and Environmental formulas that often
72
+ * caused the latter to be 0.1 lower than than the former when all Environmental
73
+ * metrics are "Not defined". Also added a RoundUp1 function to simplify formulas.
74
+ *
75
+ * 2015-04-09 Darius Wiles Added calculateCVSSFromVector function, license information, cleaned up code and improved
76
+ * comments.
77
+ *
78
+ * 2014-12-12 Darius Wiles Initial release for CVSS 3.0 Preview 2.
79
+ */
80
+
81
+ // Constants used in the formula. They are not declared as "const" to avoid problems in older browsers.
82
+
83
+ var CVSS31 = {};
84
+
85
+ CVSS31.CVSSVersionIdentifier = 'CVSS:3.1';
86
+ CVSS31.exploitabilityCoefficient = 8.22;
87
+ CVSS31.scopeCoefficient = 1.08;
88
+
89
+ // A regular expression to validate that a CVSS 3.1 vector string is well formed. It checks metrics and metric
90
+ // values. It does not check that a metric is specified more than once and it does not check that all base
91
+ // metrics are present. These checks need to be performed separately.
92
+
93
+ CVSS31.vectorStringRegex_31 =
94
+ /^CVSS:3\.[01]\/((AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])\/)*(AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$/;
95
+
96
+ // Associative arrays mapping each metric value to the constant defined in the CVSS scoring formula in the CVSS v3.1
97
+ // specification.
98
+
99
+ CVSS31.Weight = {
100
+ AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
101
+ AC: { H: 0.44, L: 0.77 },
102
+ PR: {
103
+ U: { N: 0.85, L: 0.62, H: 0.27 }, // These values are used if Scope is Unchanged
104
+ C: { N: 0.85, L: 0.68, H: 0.5 },
105
+ }, // These values are used if Scope is Changed
106
+ UI: { N: 0.85, R: 0.62 },
107
+ S: { U: 6.42, C: 7.52 }, // Note: not defined as constants in specification
108
+ CIA: { N: 0, L: 0.22, H: 0.56 }, // C, I and A have the same weights
109
+
110
+ E: { X: 1, U: 0.91, P: 0.94, F: 0.97, H: 1 },
111
+ RL: { X: 1, O: 0.95, T: 0.96, W: 0.97, U: 1 },
112
+ RC: { X: 1, U: 0.92, R: 0.96, C: 1 },
113
+
114
+ CIAR: { X: 1, L: 0.5, M: 1, H: 1.5 }, // CR, IR and AR have the same weights
115
+ };
116
+
117
+ // Severity rating bands, as defined in the CVSS v3.1 specification.
118
+
119
+ CVSS31.severityRatings = [
120
+ { name: 'None', bottom: 0.0, top: 0.0 },
121
+ { name: 'Low', bottom: 0.1, top: 3.9 },
122
+ { name: 'Medium', bottom: 4.0, top: 6.9 },
123
+ { name: 'High', bottom: 7.0, top: 8.9 },
124
+ { name: 'Critical', bottom: 9.0, top: 10.0 },
125
+ ];
126
+
127
+ /* ** CVSS31.calculateCVSSFromMetrics **
128
+ *
129
+ * Takes Base, Temporal and Environmental metric values as individual parameters. Their values are in the short format
130
+ * defined in the CVSS v3.1 standard definition of the Vector String. For example, the AttackComplexity parameter
131
+ * should be either "H" or "L".
132
+ *
133
+ * Returns Base, Temporal and Environmental scores, severity ratings, and an overall Vector String. All Base metrics
134
+ * are required to generate this output. All Temporal and Environmental metric values are optional. Any that are not
135
+ * passed default to "X" ("Not Defined").
136
+ *
137
+ * The output is an object which always has a property named "success".
138
+ *
139
+ * If no errors are encountered, success is Boolean "true", and the following other properties are defined containing
140
+ * scores, severities and a vector string:
141
+ * baseMetricScore, baseSeverity,
142
+ * temporalMetricScore, temporalSeverity,
143
+ * environmentalMetricScore, environmentalSeverity,
144
+ * vectorString
145
+ *
146
+ * The following properties are also defined, and contain sub-formula values:
147
+ * baseISS, baseImpact, baseExploitability,
148
+ * environmentalMISS, environmentalModifiedImpact, environmentalModifiedExploitability
149
+ *
150
+ *
151
+ * If errors are encountered, success is Boolean "false", and the following other properties are defined:
152
+ * errorType - a string indicating the error. Either:
153
+ * "MissingBaseMetric", if at least one Base metric has not been defined; or
154
+ * "UnknownMetricValue", if at least one metric value is invalid.
155
+ * errorMetrics - an array of strings representing the metrics at fault. The strings are abbreviated versions of the
156
+ * metrics, as defined in the CVSS v3.1 standard definition of the Vector String.
157
+ */
158
+ CVSS31.calculateCVSSFromMetrics = function (
159
+ AttackVector,
160
+ AttackComplexity,
161
+ PrivilegesRequired,
162
+ UserInteraction,
163
+ Scope,
164
+ Confidentiality,
165
+ Integrity,
166
+ Availability,
167
+ ExploitCodeMaturity,
168
+ RemediationLevel,
169
+ ReportConfidence,
170
+ ConfidentialityRequirement,
171
+ IntegrityRequirement,
172
+ AvailabilityRequirement,
173
+ ModifiedAttackVector,
174
+ ModifiedAttackComplexity,
175
+ ModifiedPrivilegesRequired,
176
+ ModifiedUserInteraction,
177
+ ModifiedScope,
178
+ ModifiedConfidentiality,
179
+ ModifiedIntegrity,
180
+ ModifiedAvailability,
181
+ ) {
182
+ // If input validation fails, this array is populated with strings indicating which metrics failed validation.
183
+ var badMetrics = [];
184
+
185
+ // ENSURE ALL BASE METRICS ARE DEFINED
186
+ //
187
+ // We need values for all Base Score metrics to calculate scores.
188
+ // If any Base Score parameters are undefined, create an array of missing metrics and return it with an error.
189
+
190
+ if (typeof AttackVector === 'undefined' || AttackVector === '') {
191
+ badMetrics.push('AV');
192
+ }
193
+ if (typeof AttackComplexity === 'undefined' || AttackComplexity === '') {
194
+ badMetrics.push('AC');
195
+ }
196
+ if (typeof PrivilegesRequired === 'undefined' || PrivilegesRequired === '') {
197
+ badMetrics.push('PR');
198
+ }
199
+ if (typeof UserInteraction === 'undefined' || UserInteraction === '') {
200
+ badMetrics.push('UI');
201
+ }
202
+ if (typeof Scope === 'undefined' || Scope === '') {
203
+ badMetrics.push('S');
204
+ }
205
+ if (typeof Confidentiality === 'undefined' || Confidentiality === '') {
206
+ badMetrics.push('C');
207
+ }
208
+ if (typeof Integrity === 'undefined' || Integrity === '') {
209
+ badMetrics.push('I');
210
+ }
211
+ if (typeof Availability === 'undefined' || Availability === '') {
212
+ badMetrics.push('A');
213
+ }
214
+
215
+ if (badMetrics.length > 0) {
216
+ return {
217
+ success: false,
218
+ errorType: 'MissingBaseMetric',
219
+ errorMetrics: badMetrics,
220
+ };
221
+ }
222
+
223
+ // STORE THE METRIC VALUES THAT WERE PASSED AS PARAMETERS
224
+ //
225
+ // Temporal and Environmental metrics are optional, so set them to "X" ("Not Defined") if no value was passed.
226
+
227
+ var AV = AttackVector;
228
+ var AC = AttackComplexity;
229
+ var PR = PrivilegesRequired;
230
+ var UI = UserInteraction;
231
+ var S = Scope;
232
+ var C = Confidentiality;
233
+ var I = Integrity;
234
+ var A = Availability;
235
+
236
+ var E = ExploitCodeMaturity || 'X';
237
+ var RL = RemediationLevel || 'X';
238
+ var RC = ReportConfidence || 'X';
239
+
240
+ var CR = ConfidentialityRequirement || 'X';
241
+ var IR = IntegrityRequirement || 'X';
242
+ var AR = AvailabilityRequirement || 'X';
243
+ var MAV = ModifiedAttackVector || 'X';
244
+ var MAC = ModifiedAttackComplexity || 'X';
245
+ var MPR = ModifiedPrivilegesRequired || 'X';
246
+ var MUI = ModifiedUserInteraction || 'X';
247
+ var MS = ModifiedScope || 'X';
248
+ var MC = ModifiedConfidentiality || 'X';
249
+ var MI = ModifiedIntegrity || 'X';
250
+ var MA = ModifiedAvailability || 'X';
251
+
252
+ // CHECK VALIDITY OF METRIC VALUES
253
+ //
254
+ // Use the Weight object to ensure that, for every metric, the metric value passed is valid.
255
+ // If any invalid values are found, create an array of their metrics and return it with an error.
256
+ //
257
+ // The Privileges Required (PR) weight depends on Scope, but when checking the validity of PR we must not assume
258
+ // that the given value for Scope is valid. We therefore always look at the weights for Unchanged Scope when
259
+ // performing this check. The same applies for validation of Modified Privileges Required (MPR).
260
+ //
261
+ // The Weights object does not contain "X" ("Not Defined") values for Environmental metrics because we replace them
262
+ // with their Base metric equivalents later in the function. For example, an MAV of "X" will be replaced with the
263
+ // value given for AV. We therefore need to explicitly allow a value of "X" for Environmental metrics.
264
+
265
+ if (!CVSS31.Weight.AV.hasOwnProperty(AV)) {
266
+ badMetrics.push('AV');
267
+ }
268
+ if (!CVSS31.Weight.AC.hasOwnProperty(AC)) {
269
+ badMetrics.push('AC');
270
+ }
271
+ if (!CVSS31.Weight.PR.U.hasOwnProperty(PR)) {
272
+ badMetrics.push('PR');
273
+ }
274
+ if (!CVSS31.Weight.UI.hasOwnProperty(UI)) {
275
+ badMetrics.push('UI');
276
+ }
277
+ if (!CVSS31.Weight.S.hasOwnProperty(S)) {
278
+ badMetrics.push('S');
279
+ }
280
+ if (!CVSS31.Weight.CIA.hasOwnProperty(C)) {
281
+ badMetrics.push('C');
282
+ }
283
+ if (!CVSS31.Weight.CIA.hasOwnProperty(I)) {
284
+ badMetrics.push('I');
285
+ }
286
+ if (!CVSS31.Weight.CIA.hasOwnProperty(A)) {
287
+ badMetrics.push('A');
288
+ }
289
+
290
+ if (!CVSS31.Weight.E.hasOwnProperty(E)) {
291
+ badMetrics.push('E');
292
+ }
293
+ if (!CVSS31.Weight.RL.hasOwnProperty(RL)) {
294
+ badMetrics.push('RL');
295
+ }
296
+ if (!CVSS31.Weight.RC.hasOwnProperty(RC)) {
297
+ badMetrics.push('RC');
298
+ }
299
+
300
+ if (!(CR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(CR))) {
301
+ badMetrics.push('CR');
302
+ }
303
+ if (!(IR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(IR))) {
304
+ badMetrics.push('IR');
305
+ }
306
+ if (!(AR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(AR))) {
307
+ badMetrics.push('AR');
308
+ }
309
+ if (!(MAV === 'X' || CVSS31.Weight.AV.hasOwnProperty(MAV))) {
310
+ badMetrics.push('MAV');
311
+ }
312
+ if (!(MAC === 'X' || CVSS31.Weight.AC.hasOwnProperty(MAC))) {
313
+ badMetrics.push('MAC');
314
+ }
315
+ if (!(MPR === 'X' || CVSS31.Weight.PR.U.hasOwnProperty(MPR))) {
316
+ badMetrics.push('MPR');
317
+ }
318
+ if (!(MUI === 'X' || CVSS31.Weight.UI.hasOwnProperty(MUI))) {
319
+ badMetrics.push('MUI');
320
+ }
321
+ if (!(MS === 'X' || CVSS31.Weight.S.hasOwnProperty(MS))) {
322
+ badMetrics.push('MS');
323
+ }
324
+ if (!(MC === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MC))) {
325
+ badMetrics.push('MC');
326
+ }
327
+ if (!(MI === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MI))) {
328
+ badMetrics.push('MI');
329
+ }
330
+ if (!(MA === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MA))) {
331
+ badMetrics.push('MA');
332
+ }
333
+
334
+ if (badMetrics.length > 0) {
335
+ return {
336
+ success: false,
337
+ errorType: 'UnknownMetricValue',
338
+ errorMetrics: badMetrics,
339
+ };
340
+ }
341
+
342
+ // GATHER WEIGHTS FOR ALL METRICS
343
+
344
+ var metricWeightAV = CVSS31.Weight.AV[AV];
345
+ var metricWeightAC = CVSS31.Weight.AC[AC];
346
+ var metricWeightPR = CVSS31.Weight.PR[S][PR]; // PR depends on the value of Scope (S).
347
+ var metricWeightUI = CVSS31.Weight.UI[UI];
348
+ var metricWeightS = CVSS31.Weight.S[S];
349
+ var metricWeightC = CVSS31.Weight.CIA[C];
350
+ var metricWeightI = CVSS31.Weight.CIA[I];
351
+ var metricWeightA = CVSS31.Weight.CIA[A];
352
+
353
+ var metricWeightE = CVSS31.Weight.E[E];
354
+ var metricWeightRL = CVSS31.Weight.RL[RL];
355
+ var metricWeightRC = CVSS31.Weight.RC[RC];
356
+
357
+ // For metrics that are modified versions of Base Score metrics, e.g. Modified Attack Vector, use the value of
358
+ // the Base Score metric if the modified version value is "X" ("Not Defined").
359
+ var metricWeightCR = CVSS31.Weight.CIAR[CR];
360
+ var metricWeightIR = CVSS31.Weight.CIAR[IR];
361
+ var metricWeightAR = CVSS31.Weight.CIAR[AR];
362
+ var metricWeightMAV = CVSS31.Weight.AV[MAV !== 'X' ? MAV : AV];
363
+ var metricWeightMAC = CVSS31.Weight.AC[MAC !== 'X' ? MAC : AC];
364
+ var metricWeightMPR =
365
+ CVSS31.Weight.PR[MS !== 'X' ? MS : S][MPR !== 'X' ? MPR : PR]; // Depends on MS.
366
+ var metricWeightMUI = CVSS31.Weight.UI[MUI !== 'X' ? MUI : UI];
367
+ var metricWeightMS = CVSS31.Weight.S[MS !== 'X' ? MS : S];
368
+ var metricWeightMC = CVSS31.Weight.CIA[MC !== 'X' ? MC : C];
369
+ var metricWeightMI = CVSS31.Weight.CIA[MI !== 'X' ? MI : I];
370
+ var metricWeightMA = CVSS31.Weight.CIA[MA !== 'X' ? MA : A];
371
+
372
+ // CALCULATE THE CVSS BASE SCORE
373
+
374
+ var iss; /* Impact Sub-Score */
375
+ var impact;
376
+ var exploitability;
377
+ var baseScore;
378
+
379
+ iss = 1 - (1 - metricWeightC) * (1 - metricWeightI) * (1 - metricWeightA);
380
+
381
+ if (S === 'U') {
382
+ impact = metricWeightS * iss;
383
+ } else {
384
+ impact = metricWeightS * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
385
+ }
386
+
387
+ exploitability =
388
+ CVSS31.exploitabilityCoefficient *
389
+ metricWeightAV *
390
+ metricWeightAC *
391
+ metricWeightPR *
392
+ metricWeightUI;
393
+
394
+ if (impact <= 0) {
395
+ baseScore = 0;
396
+ } else {
397
+ if (S === 'U') {
398
+ baseScore = CVSS31.roundUp1(Math.min(exploitability + impact, 10));
399
+ } else {
400
+ baseScore = CVSS31.roundUp1(
401
+ Math.min(CVSS31.scopeCoefficient * (exploitability + impact), 10),
402
+ );
403
+ }
404
+ }
405
+
406
+ // CALCULATE THE CVSS TEMPORAL SCORE
407
+
408
+ var temporalScore = CVSS31.roundUp1(
409
+ baseScore * metricWeightE * metricWeightRL * metricWeightRC,
410
+ );
411
+
412
+ // CALCULATE THE CVSS ENVIRONMENTAL SCORE
413
+ //
414
+ // - modifiedExploitability recalculates the Base Score Exploitability sub-score using any modified values from the
415
+ // Environmental metrics group in place of the values specified in the Base Score, if any have been defined.
416
+ // - modifiedImpact recalculates the Base Score Impact sub-score using any modified values from the
417
+ // Environmental metrics group in place of the values specified in the Base Score, and any additional weightings
418
+ // given in the Environmental metrics group.
419
+
420
+ var miss; /* Modified Impact Sub-Score */
421
+ var modifiedImpact;
422
+ var envScore;
423
+ var modifiedExploitability;
424
+
425
+ miss = Math.min(
426
+ 1 -
427
+ (1 - metricWeightMC * metricWeightCR) *
428
+ (1 - metricWeightMI * metricWeightIR) *
429
+ (1 - metricWeightMA * metricWeightAR),
430
+ 0.915,
431
+ );
432
+
433
+ if (MS === 'U' || (MS === 'X' && S === 'U')) {
434
+ modifiedImpact = metricWeightMS * miss;
435
+ } else {
436
+ modifiedImpact =
437
+ metricWeightMS * (miss - 0.029) -
438
+ 3.25 * Math.pow(miss * 0.9731 - 0.02, 13);
439
+ }
440
+
441
+ modifiedExploitability =
442
+ CVSS31.exploitabilityCoefficient *
443
+ metricWeightMAV *
444
+ metricWeightMAC *
445
+ metricWeightMPR *
446
+ metricWeightMUI;
447
+
448
+ if (modifiedImpact <= 0) {
449
+ envScore = 0;
450
+ } else if (MS === 'U' || (MS === 'X' && S === 'U')) {
451
+ envScore = CVSS31.roundUp1(
452
+ CVSS31.roundUp1(Math.min(modifiedImpact + modifiedExploitability, 10)) *
453
+ metricWeightE *
454
+ metricWeightRL *
455
+ metricWeightRC,
456
+ );
457
+ } else {
458
+ envScore = CVSS31.roundUp1(
459
+ CVSS31.roundUp1(
460
+ Math.min(
461
+ CVSS31.scopeCoefficient * (modifiedImpact + modifiedExploitability),
462
+ 10,
463
+ ),
464
+ ) *
465
+ metricWeightE *
466
+ metricWeightRL *
467
+ metricWeightRC,
468
+ );
469
+ }
470
+
471
+ // CONSTRUCT THE VECTOR STRING
472
+
473
+ var vectorString =
474
+ CVSS31.CVSSVersionIdentifier +
475
+ '/AV:' +
476
+ AV +
477
+ '/AC:' +
478
+ AC +
479
+ '/PR:' +
480
+ PR +
481
+ '/UI:' +
482
+ UI +
483
+ '/S:' +
484
+ S +
485
+ '/C:' +
486
+ C +
487
+ '/I:' +
488
+ I +
489
+ '/A:' +
490
+ A;
491
+
492
+ if (E !== 'X') {
493
+ vectorString = vectorString + '/E:' + E;
494
+ }
495
+ if (RL !== 'X') {
496
+ vectorString = vectorString + '/RL:' + RL;
497
+ }
498
+ if (RC !== 'X') {
499
+ vectorString = vectorString + '/RC:' + RC;
500
+ }
501
+
502
+ if (CR !== 'X') {
503
+ vectorString = vectorString + '/CR:' + CR;
504
+ }
505
+ if (IR !== 'X') {
506
+ vectorString = vectorString + '/IR:' + IR;
507
+ }
508
+ if (AR !== 'X') {
509
+ vectorString = vectorString + '/AR:' + AR;
510
+ }
511
+ if (MAV !== 'X') {
512
+ vectorString = vectorString + '/MAV:' + MAV;
513
+ }
514
+ if (MAC !== 'X') {
515
+ vectorString = vectorString + '/MAC:' + MAC;
516
+ }
517
+ if (MPR !== 'X') {
518
+ vectorString = vectorString + '/MPR:' + MPR;
519
+ }
520
+ if (MUI !== 'X') {
521
+ vectorString = vectorString + '/MUI:' + MUI;
522
+ }
523
+ if (MS !== 'X') {
524
+ vectorString = vectorString + '/MS:' + MS;
525
+ }
526
+ if (MC !== 'X') {
527
+ vectorString = vectorString + '/MC:' + MC;
528
+ }
529
+ if (MI !== 'X') {
530
+ vectorString = vectorString + '/MI:' + MI;
531
+ }
532
+ if (MA !== 'X') {
533
+ vectorString = vectorString + '/MA:' + MA;
534
+ }
535
+
536
+ // Return an object containing the scores for all three metric groups, and an overall vector string.
537
+ // Sub-formula values are also included.
538
+
539
+ return {
540
+ success: true,
541
+
542
+ baseMetricScore: baseScore.toFixed(1),
543
+ baseSeverity: CVSS31.severityRating(baseScore.toFixed(1)),
544
+ baseISS: iss,
545
+ baseImpact: impact,
546
+ baseExploitability: exploitability,
547
+
548
+ temporalMetricScore: temporalScore.toFixed(1),
549
+ temporalSeverity: CVSS31.severityRating(temporalScore.toFixed(1)),
550
+
551
+ environmentalMetricScore: envScore.toFixed(1),
552
+ environmentalSeverity: CVSS31.severityRating(envScore.toFixed(1)),
553
+ environmentalMISS: miss,
554
+ environmentalModifiedImpact: modifiedImpact,
555
+ environmentalModifiedExploitability: modifiedExploitability,
556
+
557
+ vectorString: vectorString,
558
+ };
559
+ };
560
+
561
+ /* ** CVSS31.calculateCVSSFromVector **
562
+ *
563
+ * Takes Base, Temporal and Environmental metric values as a single string in the Vector String format defined
564
+ * in the CVSS v3.1 standard definition of the Vector String.
565
+ *
566
+ * Returns Base, Temporal and Environmental scores, severity ratings, and an overall Vector String. All Base metrics
567
+ * are required to generate this output. All Temporal and Environmental metric values are optional. Any that are not
568
+ * passed default to "X" ("Not Defined").
569
+ *
570
+ * See the comment for the CVSS31.calculateCVSSFromMetrics function for details on the function output. In addition to
571
+ * the error conditions listed for that function, this function can also return:
572
+ * "MalformedVectorString", if the Vector String passed does not conform to the format in the standard; or
573
+ * "MultipleDefinitionsOfMetric", if the Vector String is well formed but defines the same metric (or metrics),
574
+ * more than once.
575
+ */
576
+ CVSS31.calculateCVSSFromVector = function (vectorString) {
577
+ var metricValues = {
578
+ AV: undefined,
579
+ AC: undefined,
580
+ PR: undefined,
581
+ UI: undefined,
582
+ S: undefined,
583
+ C: undefined,
584
+ I: undefined,
585
+ A: undefined,
586
+ E: undefined,
587
+ RL: undefined,
588
+ RC: undefined,
589
+ CR: undefined,
590
+ IR: undefined,
591
+ AR: undefined,
592
+ MAV: undefined,
593
+ MAC: undefined,
594
+ MPR: undefined,
595
+ MUI: undefined,
596
+ MS: undefined,
597
+ MC: undefined,
598
+ MI: undefined,
599
+ MA: undefined,
600
+ };
601
+
602
+ // If input validation fails, this array is populated with strings indicating which metrics failed validation.
603
+ var badMetrics = [];
604
+
605
+ if (!CVSS31.vectorStringRegex_31.test(vectorString)) {
606
+ return { success: false, errorType: 'MalformedVectorString' };
607
+ }
608
+
609
+ var metricNameValue = vectorString
610
+ .substring(CVSS31.CVSSVersionIdentifier.length)
611
+ .split('/');
612
+
613
+ for (var i in metricNameValue) {
614
+ if (metricNameValue.hasOwnProperty(i)) {
615
+ var singleMetric = metricNameValue[i].split(':');
616
+
617
+ if (typeof metricValues[singleMetric[0]] === 'undefined') {
618
+ metricValues[singleMetric[0]] = singleMetric[1];
619
+ } else {
620
+ badMetrics.push(singleMetric[0]);
621
+ }
622
+ }
623
+ }
624
+
625
+ if (badMetrics.length > 0) {
626
+ return {
627
+ success: false,
628
+ errorType: 'MultipleDefinitionsOfMetric',
629
+ errorMetrics: badMetrics,
630
+ };
631
+ }
632
+
633
+ return CVSS31.calculateCVSSFromMetrics(
634
+ metricValues.AV,
635
+ metricValues.AC,
636
+ metricValues.PR,
637
+ metricValues.UI,
638
+ metricValues.S,
639
+ metricValues.C,
640
+ metricValues.I,
641
+ metricValues.A,
642
+ metricValues.E,
643
+ metricValues.RL,
644
+ metricValues.RC,
645
+ metricValues.CR,
646
+ metricValues.IR,
647
+ metricValues.AR,
648
+ metricValues.MAV,
649
+ metricValues.MAC,
650
+ metricValues.MPR,
651
+ metricValues.MUI,
652
+ metricValues.MS,
653
+ metricValues.MC,
654
+ metricValues.MI,
655
+ metricValues.MA,
656
+ );
657
+ };
658
+
659
+ /* ** CVSS31.roundUp1 **
660
+ *
661
+ * Rounds up its parameter to 1 decimal place and returns the result.
662
+ *
663
+ * Standard JavaScript errors thrown when arithmetic operations are performed on non-numbers will be returned if the
664
+ * given input is not a number.
665
+ *
666
+ * Implementation note: Tiny representation errors in floating point numbers makes rounding complex. For example,
667
+ * consider calculating Math.ceil((1-0.58)*100) by hand. It can be simplified to Math.ceil(0.42*100), then
668
+ * Math.ceil(42), and finally 42. Most JavaScript implementations give 43. The problem is that, on many systems,
669
+ * 1-0.58 = 0.42000000000000004, and the tiny error is enough to push ceil up to the next integer. The implementation
670
+ * below avoids such problems by performing the rounding using integers. The input is first multiplied by 100,000
671
+ * and rounded to the nearest integer to consider 6 decimal places of accuracy, so 0.000001 results in 0.0, but
672
+ * 0.000009 results in 0.1.
673
+ *
674
+ * A more elegant solution may be possible, but the following gives answers consistent with results from an arbitrary
675
+ * precision library.
676
+ */
677
+ CVSS31.roundUp1 = function Roundup(input) {
678
+ var int_input = Math.round(input * 100000);
679
+
680
+ if (int_input % 10000 === 0) {
681
+ return int_input / 100000;
682
+ } else {
683
+ return (Math.floor(int_input / 10000) + 1) / 10;
684
+ }
685
+ };
686
+
687
+ /* ** CVSS31.severityRating **
688
+ *
689
+ * Given a CVSS score, returns the name of the severity rating as defined in the CVSS standard.
690
+ * The input needs to be a number between 0.0 to 10.0, to one decimal place of precision.
691
+ *
692
+ * The following error values may be returned instead of a severity rating name:
693
+ * NaN (JavaScript "Not a Number") - if the input is not a number.
694
+ * undefined - if the input is a number that is not within the range of any defined severity rating.
695
+ */
696
+ CVSS31.severityRating = function (score) {
697
+ var severityRatingLength = CVSS31.severityRatings.length;
698
+
699
+ var validatedScore = Number(score);
700
+
701
+ if (isNaN(validatedScore)) {
702
+ return validatedScore;
703
+ }
704
+
705
+ for (var i = 0; i < severityRatingLength; i++) {
706
+ if (
707
+ score >= CVSS31.severityRatings[i].bottom &&
708
+ score <= CVSS31.severityRatings[i].top
709
+ ) {
710
+ return CVSS31.severityRatings[i].name;
711
+ }
712
+ }
713
+
714
+ return undefined;
715
+ };
716
+
717
+ ///////////////////////////////////////////////////////////////////////////
718
+ // DATA AND FUNCTIONS FOR CREATING AN XML REPRESENTATION OF A CVSS SCORE //
719
+ ///////////////////////////////////////////////////////////////////////////
720
+
721
+ // A mapping between abbreviated metric values and the string used in the XML representation.
722
+ // For example, a Remediation Level (RL) abbreviated metric value of "W" maps to "WORKAROUND".
723
+ // For brevity, every Base metric shares its definition with its equivalent Environmental metric. This is possible
724
+ // because the metric values are same between these groups, except that the latter have an additional metric value
725
+ // of "NOT_DEFINED".
726
+
727
+ CVSS31.XML_MetricNames = {
728
+ E: {
729
+ X: 'NOT_DEFINED',
730
+ U: 'UNPROVEN',
731
+ P: 'PROOF_OF_CONCEPT',
732
+ F: 'FUNCTIONAL',
733
+ H: 'HIGH',
734
+ },
735
+ RL: {
736
+ X: 'NOT_DEFINED',
737
+ O: 'OFFICIAL_FIX',
738
+ T: 'TEMPORARY_FIX',
739
+ W: 'WORKAROUND',
740
+ U: 'UNAVAILABLE',
741
+ },
742
+ RC: { X: 'NOT_DEFINED', U: 'UNKNOWN', R: 'REASONABLE', C: 'CONFIRMED' },
743
+
744
+ CIAR: { X: 'NOT_DEFINED', L: 'LOW', M: 'MEDIUM', H: 'HIGH' }, // CR, IR and AR use the same values
745
+ MAV: {
746
+ N: 'NETWORK',
747
+ A: 'ADJACENT_NETWORK',
748
+ L: 'LOCAL',
749
+ P: 'PHYSICAL',
750
+ X: 'NOT_DEFINED',
751
+ },
752
+ MAC: { H: 'HIGH', L: 'LOW', X: 'NOT_DEFINED' },
753
+ MPR: { N: 'NONE', L: 'LOW', H: 'HIGH', X: 'NOT_DEFINED' },
754
+ MUI: { N: 'NONE', R: 'REQUIRED', X: 'NOT_DEFINED' },
755
+ MS: { U: 'UNCHANGED', C: 'CHANGED', X: 'NOT_DEFINED' },
756
+ MCIA: { N: 'NONE', L: 'LOW', H: 'HIGH', X: 'NOT_DEFINED' }, // C, I and A use the same values
757
+ };
758
+
759
+ /* ** CVSS31.generateXMLFromMetrics **
760
+ *
761
+ * Takes Base, Temporal and Environmental metric values as individual parameters. Their values are in the short format
762
+ * defined in the CVSS v3.1 standard definition of the Vector String. For example, the AttackComplexity parameter
763
+ * should be either "H" or "L".
764
+ *
765
+ * Returns a single string containing the metric values in XML form. All Base metrics are required to generate this
766
+ * output. All Temporal and Environmental metric values are optional. Any that are not passed will be represented in
767
+ * the XML as NOT_DEFINED. The function returns a string for simplicity. It is arguably better to return the XML as
768
+ * a DOM object, but at the time of writing this leads to complexity due to older browsers using different JavaScript
769
+ * interfaces to do this. Also for simplicity, all Temporal and Environmental metrics are included in the string,
770
+ * even though those with a value of "Not Defined" do not need to be included.
771
+ *
772
+ * The output of this function is an object which always has a property named "success".
773
+ *
774
+ * If no errors are encountered, success is Boolean "true", and the "xmlString" property contains the XML string
775
+ * representation.
776
+ *
777
+ * If errors are encountered, success is Boolean "false", and other properties are defined as per the
778
+ * CVSS31.calculateCVSSFromMetrics function. Refer to the comment for that function for more details.
779
+ */
780
+ CVSS31.generateXMLFromMetrics = function (
781
+ AttackVector,
782
+ AttackComplexity,
783
+ PrivilegesRequired,
784
+ UserInteraction,
785
+ Scope,
786
+ Confidentiality,
787
+ Integrity,
788
+ Availability,
789
+ ExploitCodeMaturity,
790
+ RemediationLevel,
791
+ ReportConfidence,
792
+ ConfidentialityRequirement,
793
+ IntegrityRequirement,
794
+ AvailabilityRequirement,
795
+ ModifiedAttackVector,
796
+ ModifiedAttackComplexity,
797
+ ModifiedPrivilegesRequired,
798
+ ModifiedUserInteraction,
799
+ ModifiedScope,
800
+ ModifiedConfidentiality,
801
+ ModifiedIntegrity,
802
+ ModifiedAvailability,
803
+ ) {
804
+ // A string containing the XML we wish to output, with placeholders for the CVSS metrics we will substitute for
805
+ // their values, based on the inputs passed to this function.
806
+ var xmlTemplate =
807
+ '<?xml version="1.0" encoding="UTF-8"?>\n' +
808
+ '<cvssv3.1 xmlns="https://www.first.org/cvss/cvss-v3.1.xsd"\n' +
809
+ ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' +
810
+ ' xsi:schemaLocation="https://www.first.org/cvss/cvss-v3.1.xsd https://www.first.org/cvss/cvss-v3.1.xsd"\n' +
811
+ ' >\n' +
812
+ '\n' +
813
+ ' <base_metrics>\n' +
814
+ ' <attack-vector>__AttackVector__</attack-vector>\n' +
815
+ ' <attack-complexity>__AttackComplexity__</attack-complexity>\n' +
816
+ ' <privileges-required>__PrivilegesRequired__</privileges-required>\n' +
817
+ ' <user-interaction>__UserInteraction__</user-interaction>\n' +
818
+ ' <scope>__Scope__</scope>\n' +
819
+ ' <confidentiality-impact>__Confidentiality__</confidentiality-impact>\n' +
820
+ ' <integrity-impact>__Integrity__</integrity-impact>\n' +
821
+ ' <availability-impact>__Availability__</availability-impact>\n' +
822
+ ' <base-score>__BaseScore__</base-score>\n' +
823
+ ' <base-severity>__BaseSeverityRating__</base-severity>\n' +
824
+ ' </base_metrics>\n' +
825
+ '\n' +
826
+ ' <temporal_metrics>\n' +
827
+ ' <exploit-code-maturity>__ExploitCodeMaturity__</exploit-code-maturity>\n' +
828
+ ' <remediation-level>__RemediationLevel__</remediation-level>\n' +
829
+ ' <report-confidence>__ReportConfidence__</report-confidence>\n' +
830
+ ' <temporal-score>__TemporalScore__</temporal-score>\n' +
831
+ ' <temporal-severity>__TemporalSeverityRating__</temporal-severity>\n' +
832
+ ' </temporal_metrics>\n' +
833
+ '\n' +
834
+ ' <environmental_metrics>\n' +
835
+ ' <confidentiality-requirement>__ConfidentialityRequirement__</confidentiality-requirement>\n' +
836
+ ' <integrity-requirement>__IntegrityRequirement__</integrity-requirement>\n' +
837
+ ' <availability-requirement>__AvailabilityRequirement__</availability-requirement>\n' +
838
+ ' <modified-attack-vector>__ModifiedAttackVector__</modified-attack-vector>\n' +
839
+ ' <modified-attack-complexity>__ModifiedAttackComplexity__</modified-attack-complexity>\n' +
840
+ ' <modified-privileges-required>__ModifiedPrivilegesRequired__</modified-privileges-required>\n' +
841
+ ' <modified-user-interaction>__ModifiedUserInteraction__</modified-user-interaction>\n' +
842
+ ' <modified-scope>__ModifiedScope__</modified-scope>\n' +
843
+ ' <modified-confidentiality-impact>__ModifiedConfidentiality__</modified-confidentiality-impact>\n' +
844
+ ' <modified-integrity-impact>__ModifiedIntegrity__</modified-integrity-impact>\n' +
845
+ ' <modified-availability-impact>__ModifiedAvailability__</modified-availability-impact>\n' +
846
+ ' <environmental-score>__EnvironmentalScore__</environmental-score>\n' +
847
+ ' <environmental-severity>__EnvironmentalSeverityRating__</environmental-severity>\n' +
848
+ ' </environmental_metrics>\n' +
849
+ '\n' +
850
+ '</cvssv3.1>\n';
851
+
852
+ // Call CVSS31.calculateCVSSFromMetrics to validate all the parameters and generate scores and severity ratings.
853
+ // If that function returns an error, immediately return it to the caller of this function.
854
+ var result = CVSS31.calculateCVSSFromMetrics(
855
+ AttackVector,
856
+ AttackComplexity,
857
+ PrivilegesRequired,
858
+ UserInteraction,
859
+ Scope,
860
+ Confidentiality,
861
+ Integrity,
862
+ Availability,
863
+ ExploitCodeMaturity,
864
+ RemediationLevel,
865
+ ReportConfidence,
866
+ ConfidentialityRequirement,
867
+ IntegrityRequirement,
868
+ AvailabilityRequirement,
869
+ ModifiedAttackVector,
870
+ ModifiedAttackComplexity,
871
+ ModifiedPrivilegesRequired,
872
+ ModifiedUserInteraction,
873
+ ModifiedScope,
874
+ ModifiedConfidentiality,
875
+ ModifiedIntegrity,
876
+ ModifiedAvailability,
877
+ );
878
+
879
+ if (result.success !== true) {
880
+ return result;
881
+ }
882
+
883
+ var xmlOutput = xmlTemplate;
884
+ xmlOutput = xmlOutput.replace(
885
+ '__AttackVector__',
886
+ CVSS31.XML_MetricNames['MAV'][AttackVector],
887
+ );
888
+ xmlOutput = xmlOutput.replace(
889
+ '__AttackComplexity__',
890
+ CVSS31.XML_MetricNames['MAC'][AttackComplexity],
891
+ );
892
+ xmlOutput = xmlOutput.replace(
893
+ '__PrivilegesRequired__',
894
+ CVSS31.XML_MetricNames['MPR'][PrivilegesRequired],
895
+ );
896
+ xmlOutput = xmlOutput.replace(
897
+ '__UserInteraction__',
898
+ CVSS31.XML_MetricNames['MUI'][UserInteraction],
899
+ );
900
+ xmlOutput = xmlOutput.replace(
901
+ '__Scope__',
902
+ CVSS31.XML_MetricNames['MS'][Scope],
903
+ );
904
+ xmlOutput = xmlOutput.replace(
905
+ '__Confidentiality__',
906
+ CVSS31.XML_MetricNames['MCIA'][Confidentiality],
907
+ );
908
+ xmlOutput = xmlOutput.replace(
909
+ '__Integrity__',
910
+ CVSS31.XML_MetricNames['MCIA'][Integrity],
911
+ );
912
+ xmlOutput = xmlOutput.replace(
913
+ '__Availability__',
914
+ CVSS31.XML_MetricNames['MCIA'][Availability],
915
+ );
916
+ xmlOutput = xmlOutput.replace('__BaseScore__', result.baseMetricScore);
917
+ xmlOutput = xmlOutput.replace('__BaseSeverityRating__', result.baseSeverity);
918
+
919
+ xmlOutput = xmlOutput.replace(
920
+ '__ExploitCodeMaturity__',
921
+ CVSS31.XML_MetricNames['E'][ExploitCodeMaturity || 'X'],
922
+ );
923
+ xmlOutput = xmlOutput.replace(
924
+ '__RemediationLevel__',
925
+ CVSS31.XML_MetricNames['RL'][RemediationLevel || 'X'],
926
+ );
927
+ xmlOutput = xmlOutput.replace(
928
+ '__ReportConfidence__',
929
+ CVSS31.XML_MetricNames['RC'][ReportConfidence || 'X'],
930
+ );
931
+ xmlOutput = xmlOutput.replace(
932
+ '__TemporalScore__',
933
+ result.temporalMetricScore,
934
+ );
935
+ xmlOutput = xmlOutput.replace(
936
+ '__TemporalSeverityRating__',
937
+ result.temporalSeverity,
938
+ );
939
+
940
+ xmlOutput = xmlOutput.replace(
941
+ '__ConfidentialityRequirement__',
942
+ CVSS31.XML_MetricNames['CIAR'][ConfidentialityRequirement || 'X'],
943
+ );
944
+ xmlOutput = xmlOutput.replace(
945
+ '__IntegrityRequirement__',
946
+ CVSS31.XML_MetricNames['CIAR'][IntegrityRequirement || 'X'],
947
+ );
948
+ xmlOutput = xmlOutput.replace(
949
+ '__AvailabilityRequirement__',
950
+ CVSS31.XML_MetricNames['CIAR'][AvailabilityRequirement || 'X'],
951
+ );
952
+ xmlOutput = xmlOutput.replace(
953
+ '__ModifiedAttackVector__',
954
+ CVSS31.XML_MetricNames['MAV'][ModifiedAttackVector || 'X'],
955
+ );
956
+ xmlOutput = xmlOutput.replace(
957
+ '__ModifiedAttackComplexity__',
958
+ CVSS31.XML_MetricNames['MAC'][ModifiedAttackComplexity || 'X'],
959
+ );
960
+ xmlOutput = xmlOutput.replace(
961
+ '__ModifiedPrivilegesRequired__',
962
+ CVSS31.XML_MetricNames['MPR'][ModifiedPrivilegesRequired || 'X'],
963
+ );
964
+ xmlOutput = xmlOutput.replace(
965
+ '__ModifiedUserInteraction__',
966
+ CVSS31.XML_MetricNames['MUI'][ModifiedUserInteraction || 'X'],
967
+ );
968
+ xmlOutput = xmlOutput.replace(
969
+ '__ModifiedScope__',
970
+ CVSS31.XML_MetricNames['MS'][ModifiedScope || 'X'],
971
+ );
972
+ xmlOutput = xmlOutput.replace(
973
+ '__ModifiedConfidentiality__',
974
+ CVSS31.XML_MetricNames['MCIA'][ModifiedConfidentiality || 'X'],
975
+ );
976
+ xmlOutput = xmlOutput.replace(
977
+ '__ModifiedIntegrity__',
978
+ CVSS31.XML_MetricNames['MCIA'][ModifiedIntegrity || 'X'],
979
+ );
980
+ xmlOutput = xmlOutput.replace(
981
+ '__ModifiedAvailability__',
982
+ CVSS31.XML_MetricNames['MCIA'][ModifiedAvailability || 'X'],
983
+ );
984
+ xmlOutput = xmlOutput.replace(
985
+ '__EnvironmentalScore__',
986
+ result.environmentalMetricScore,
987
+ );
988
+ xmlOutput = xmlOutput.replace(
989
+ '__EnvironmentalSeverityRating__',
990
+ result.environmentalSeverity,
991
+ );
992
+
993
+ return { success: true, xmlString: xmlOutput };
994
+ };
995
+
996
+ /* ** CVSS31.generateXMLFromVector **
997
+ *
998
+ * Takes Base, Temporal and Environmental metric values as a single string in the Vector String format defined
999
+ * in the CVSS v3.1 standard definition of the Vector String.
1000
+ *
1001
+ * Returns an XML string representation of this input. See the comment for CVSS31.generateXMLFromMetrics for more
1002
+ * detail on inputs, return values and errors. In addition to the error conditions listed for that function, this
1003
+ * function can also return:
1004
+ * "MalformedVectorString", if the Vector String passed is does not conform to the format in the standard; or
1005
+ * "MultipleDefinitionsOfMetric", if the Vector String is well formed but defines the same metric (or metrics),
1006
+ * more than once.
1007
+ */
1008
+ CVSS31.generateXMLFromVector = function (vectorString) {
1009
+ var metricValues = {
1010
+ AV: undefined,
1011
+ AC: undefined,
1012
+ PR: undefined,
1013
+ UI: undefined,
1014
+ S: undefined,
1015
+ C: undefined,
1016
+ I: undefined,
1017
+ A: undefined,
1018
+ E: undefined,
1019
+ RL: undefined,
1020
+ RC: undefined,
1021
+ CR: undefined,
1022
+ IR: undefined,
1023
+ AR: undefined,
1024
+ MAV: undefined,
1025
+ MAC: undefined,
1026
+ MPR: undefined,
1027
+ MUI: undefined,
1028
+ MS: undefined,
1029
+ MC: undefined,
1030
+ MI: undefined,
1031
+ MA: undefined,
1032
+ };
1033
+
1034
+ // If input validation fails, this array is populated with strings indicating which metrics failed validation.
1035
+ var badMetrics = [];
1036
+
1037
+ if (!CVSS31.vectorStringRegex_31.test(vectorString)) {
1038
+ return { success: false, errorType: 'MalformedVectorString' };
1039
+ }
1040
+
1041
+ var metricNameValue = vectorString
1042
+ .substring(CVSS31.CVSSVersionIdentifier.length)
1043
+ .split('/');
1044
+
1045
+ for (var i in metricNameValue) {
1046
+ if (metricNameValue.hasOwnProperty(i)) {
1047
+ var singleMetric = metricNameValue[i].split(':');
1048
+
1049
+ if (typeof metricValues[singleMetric[0]] === 'undefined') {
1050
+ metricValues[singleMetric[0]] = singleMetric[1];
1051
+ } else {
1052
+ badMetrics.push(singleMetric[0]);
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ if (badMetrics.length > 0) {
1058
+ return {
1059
+ success: false,
1060
+ errorType: 'MultipleDefinitionsOfMetric',
1061
+ errorMetrics: badMetrics,
1062
+ };
1063
+ }
1064
+
1065
+ return CVSS31.generateXMLFromMetrics(
1066
+ metricValues.AV,
1067
+ metricValues.AC,
1068
+ metricValues.PR,
1069
+ metricValues.UI,
1070
+ metricValues.S,
1071
+ metricValues.C,
1072
+ metricValues.I,
1073
+ metricValues.A,
1074
+ metricValues.E,
1075
+ metricValues.RL,
1076
+ metricValues.RC,
1077
+ metricValues.CR,
1078
+ metricValues.IR,
1079
+ metricValues.AR,
1080
+ metricValues.MAV,
1081
+ metricValues.MAC,
1082
+ metricValues.MPR,
1083
+ metricValues.MUI,
1084
+ metricValues.MS,
1085
+ metricValues.MC,
1086
+ metricValues.MI,
1087
+ metricValues.MA,
1088
+ );
1089
+ };
1090
+
1091
+ module.exports = CVSS31;
backend/src/lib/html2ooxml.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var docx = require('docx');
2
+ var xml = require('xml');
3
+ var htmlparser = require('htmlparser2');
4
+
5
+ function html2ooxml(html, style = '') {
6
+ if (html === '') return html;
7
+ if (!html.match(/^<.+>/)) html = `<p>${html}</p>`;
8
+ var doc = new docx.Document({ sections: [] });
9
+ var paragraphs = [];
10
+ var cParagraph = null;
11
+ var cRunProperties = {};
12
+ var cParagraphProperties = {};
13
+ var list_state = [];
14
+ var inCodeBlock = false;
15
+ var parser = new htmlparser.Parser(
16
+ {
17
+ onopentag(tag, attribs) {
18
+ if (tag === 'h1') {
19
+ cParagraph = new docx.Paragraph({ heading: 'Heading1' });
20
+ } else if (tag === 'h2') {
21
+ cParagraph = new docx.Paragraph({ heading: 'Heading2' });
22
+ } else if (tag === 'h3') {
23
+ cParagraph = new docx.Paragraph({ heading: 'Heading3' });
24
+ } else if (tag === 'h4') {
25
+ cParagraph = new docx.Paragraph({ heading: 'Heading4' });
26
+ } else if (tag === 'h5') {
27
+ cParagraph = new docx.Paragraph({ heading: 'Heading5' });
28
+ } else if (tag === 'h6') {
29
+ cParagraph = new docx.Paragraph({ heading: 'Heading6' });
30
+ } else if (tag === 'div' || tag === 'p') {
31
+ if (style && typeof style === 'string')
32
+ cParagraphProperties.style = style;
33
+ cParagraph = new docx.Paragraph(cParagraphProperties);
34
+ } else if (tag === 'pre') {
35
+ inCodeBlock = true;
36
+ cParagraph = new docx.Paragraph({ style: 'Code' });
37
+ } else if (tag === 'b' || tag === 'strong') {
38
+ cRunProperties.bold = true;
39
+ } else if (tag === 'i' || tag === 'em') {
40
+ cRunProperties.italics = true;
41
+ } else if (tag === 'u') {
42
+ cRunProperties.underline = {};
43
+ } else if (tag === 'strike' || tag === 's') {
44
+ cRunProperties.strike = true;
45
+ } else if (tag === 'mark') {
46
+ var bgColor = attribs['data-color'] || '#ffff25';
47
+ cRunProperties.highlight = getHighlightColor(bgColor);
48
+
49
+ // Use text color if set (to handle white or black text depending on background color)
50
+ var color = attribs.style.match(/.+color:.(.+)/);
51
+ if (color && color[1]) cRunProperties.color = getTextColor(color[1]);
52
+ } else if (tag === 'br') {
53
+ if (inCodeBlock) {
54
+ paragraphs.push(cParagraph);
55
+ cParagraph = new docx.Paragraph({ style: 'Code' });
56
+ } else cParagraph.addChildElement(new docx.Run({ break: 1 }));
57
+ } else if (tag === 'ul') {
58
+ list_state.push('bullet');
59
+ } else if (tag === 'ol') {
60
+ list_state.push('number');
61
+ } else if (tag === 'li') {
62
+ var level = list_state.length - 1;
63
+ if (level >= 0 && list_state[level] === 'bullet')
64
+ cParagraphProperties.bullet = { level: level };
65
+ else if (level >= 0 && list_state[level] === 'number')
66
+ cParagraphProperties.numbering = { reference: 2, level: level };
67
+ else cParagraphProperties.bullet = { level: 0 };
68
+ } else if (tag === 'code') {
69
+ cRunProperties.style = 'CodeChar';
70
+ } else if (tag === 'legend' && attribs && attribs.alt !== 'undefined') {
71
+ var label = attribs.label || 'Figure';
72
+ cParagraph = new docx.Paragraph({
73
+ style: 'Caption',
74
+ alignment: docx.AlignmentType.CENTER,
75
+ });
76
+ cParagraph.addChildElement(new docx.TextRun(`${label} `));
77
+ cParagraph.addChildElement(new docx.SimpleField(`SEQ ${label}`, '1'));
78
+ cParagraph.addChildElement(new docx.TextRun(` - ${attribs.alt}`));
79
+ }
80
+ },
81
+
82
+ ontext(text) {
83
+ if (text && cParagraph) {
84
+ cRunProperties.text = text;
85
+ cParagraph.addChildElement(new docx.TextRun(cRunProperties));
86
+ }
87
+ },
88
+
89
+ onclosetag(tag) {
90
+ if (
91
+ [
92
+ 'h1',
93
+ 'h2',
94
+ 'h3',
95
+ 'h4',
96
+ 'h5',
97
+ 'h6',
98
+ 'div',
99
+ 'p',
100
+ 'pre',
101
+ 'img',
102
+ 'legend',
103
+ ].includes(tag)
104
+ ) {
105
+ paragraphs.push(cParagraph);
106
+ cParagraph = null;
107
+ cParagraphProperties = {};
108
+ if (tag === 'pre') inCodeBlock = false;
109
+ } else if (tag === 'b' || tag === 'strong') {
110
+ delete cRunProperties.bold;
111
+ } else if (tag === 'i' || tag === 'em') {
112
+ delete cRunProperties.italics;
113
+ } else if (tag === 'u') {
114
+ delete cRunProperties.underline;
115
+ } else if (tag === 'strike' || tag === 's') {
116
+ delete cRunProperties.strike;
117
+ } else if (tag === 'mark') {
118
+ delete cRunProperties.highlight;
119
+ delete cRunProperties.color;
120
+ } else if (tag === 'ul' || tag === 'ol') {
121
+ list_state.pop();
122
+ if (list_state.length === 0) cParagraphProperties = {};
123
+ } else if (tag === 'code') {
124
+ delete cRunProperties.style;
125
+ }
126
+ },
127
+
128
+ onend() {
129
+ doc.addSection({
130
+ children: paragraphs,
131
+ });
132
+ },
133
+ },
134
+ { decodeEntities: true },
135
+ );
136
+
137
+ // For multiline code blocks
138
+ html = html.replace(/\n/g, '<br>');
139
+ parser.write(html);
140
+ parser.end();
141
+
142
+ var prepXml = doc.documentWrapper.document.body.prepForXml({});
143
+ var filteredXml = prepXml['w:body'].filter(e => {
144
+ return Object.keys(e)[0] === 'w:p';
145
+ });
146
+ var dataXml = xml(filteredXml);
147
+ dataXml = dataXml.replace(/w:numId w:val="{2-0}"/g, 'w:numId w:val="2"'); // Replace numbering to have correct value
148
+
149
+ return dataXml;
150
+ }
151
+ module.exports = html2ooxml;
152
+
153
+ function getHighlightColor(hexColor) {
154
+ // <xsd:simpleType name="ST_HighlightColor">
155
+ // <xsd:restriction base="xsd:string">
156
+ // <xsd:enumeration value="yellow"/>
157
+ // <xsd:enumeration value="green"/>
158
+ // <xsd:enumeration value="cyan"/>
159
+ // <xsd:enumeration value="magenta"/>
160
+ // <xsd:enumeration value="blue"/>
161
+
162
+ // <xsd:enumeration value="red"/>
163
+ // <xsd:enumeration value="darkBlue"/>
164
+ // <xsd:enumeration value="darkCyan"/>
165
+ // <xsd:enumeration value="darkGreen"/>
166
+ // <xsd:enumeration value="darkMagenta"/>
167
+
168
+ // <xsd:enumeration value="darkRed"/>
169
+ // <xsd:enumeration value="darkYellow"/>
170
+ // <xsd:enumeration value="darkGray"/>
171
+ // <xsd:enumeration value="lightGray"/>
172
+ // <xsd:enumeration value="black"/>
173
+
174
+ // <xsd:enumeration value="white"/>
175
+ // <xsd:enumeration value="none"/>
176
+ // </xsd:restriction>
177
+ // </xsd:simpleType>
178
+
179
+ var colors = {
180
+ '#ffff25': 'yellow',
181
+ '#00ff41': 'green',
182
+ '#00ffff': 'cyan',
183
+ '#ff00f9': 'magenta',
184
+ '#0005fd': 'blue',
185
+ '#ff0000': 'red',
186
+ '#000177': 'darkBlue',
187
+ '#00807a': 'darkCyan',
188
+ '#008021': 'darkGreen',
189
+ '#8e0075': 'darkMagenta',
190
+ '#8f0000': 'darkRed',
191
+ '#817d0c': 'darkYellow',
192
+ '#807d78': 'darkGray',
193
+ '#c4c1bb': 'lightGray',
194
+ '#000000': 'black',
195
+ };
196
+ return colors[hexColor] || 'yellow';
197
+ }
198
+
199
+ function getTextColor(color) {
200
+ var regex = /^#[0-9a-fA-F]{6}$/;
201
+ if (regex.test(color)) return color.substring(1, 7);
202
+
203
+ return '000000';
204
+ }
backend/src/lib/httpResponse.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Custom(res, status, code, message) {
2
+ res.status(code).json({ status: status, datas: message });
3
+ }
4
+ exports.Custom = Custom;
5
+
6
+ /*
7
+ *** Codes 2xx ***
8
+ */
9
+
10
+ function Ok(res, data) {
11
+ res.status(200).json({ status: 'success', datas: data });
12
+ }
13
+ exports.Ok = Ok;
14
+
15
+ function Created(res, data) {
16
+ res.status(201).json({ status: 'success', datas: data });
17
+ }
18
+ exports.Created = Created;
19
+
20
+ function NoContent(res, data) {
21
+ res.status(204).json({ status: 'success', datas: data });
22
+ }
23
+ exports.NoContent = NoContent;
24
+
25
+ function SendFile(res, filename, file) {
26
+ res.set({ 'Content-Disposition': `attachment; filename="${filename}"` });
27
+ res.status(200).send(file);
28
+ }
29
+ exports.SendFile = SendFile;
30
+
31
+ function SendImage(res, image) {
32
+ res.set({ 'Content-Type': 'image/png', 'Content-Length': image.length });
33
+ res.status(200).send(image);
34
+ }
35
+ exports.SendImage = SendImage;
36
+
37
+ /*
38
+ *** Codes 4xx ***
39
+ */
40
+
41
+ function BadRequest(res, error) {
42
+ res.status(400).json({ status: 'error', datas: error });
43
+ }
44
+ exports.BadRequest = BadRequest;
45
+
46
+ function NotFound(res, error) {
47
+ res.status(404).json({ status: 'error', datas: error });
48
+ }
49
+ exports.NotFound = NotFound;
50
+
51
+ function BadParameters(res, error) {
52
+ res.status(422).json({ status: 'error', datas: error });
53
+ }
54
+ exports.BadParameters = BadParameters;
55
+
56
+ function Unauthorized(res, error) {
57
+ res.status(401).json({ status: 'error', datas: error });
58
+ }
59
+ exports.Unauthorized = Unauthorized;
60
+
61
+ function Forbidden(res, error) {
62
+ res.status(403).json({ status: 'error', datas: error });
63
+ }
64
+ exports.Forbidden = Forbidden;
65
+
66
+ /*
67
+ *** Codes 5xx ***
68
+ */
69
+
70
+ function Internal(res, error) {
71
+ if (error.fn) var fn = exports[error.fn];
72
+ if (typeof fn === 'function') fn(res, error.message);
73
+ else if (error.errmsg) {
74
+ res.status(500).json({ status: 'error', datas: error.errmsg });
75
+ } else if (error.message)
76
+ res.status(500).json({ status: 'error', datas: error.message });
77
+ else {
78
+ console.log(error);
79
+ res.status(500).json({ status: 'error', datas: 'Internal Error' });
80
+ }
81
+ }
82
+ exports.Internal = Internal;
backend/src/lib/passwordpolicy.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ // Check if user's password match password policy
2
+ function strongPassword(password) {
3
+ var regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/;
4
+ return regExp.test(password);
5
+ }
6
+
7
+ exports.strongPassword = strongPassword;
backend/src/lib/report-filters.js ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var expressions = require('angular-expressions');
2
+ var html2ooxml = require('./html2ooxml');
3
+ var translate = require('../translate');
4
+ var _ = require('lodash');
5
+
6
+ // *** Angular parser filters ***
7
+
8
+ // Creates a text block or simple location bookmark:
9
+ // - Text block: {@name | bookmarkCreate: identifier | p}
10
+ // - Location: {@identifier | bookmarkCreate | p}
11
+ // Identifiers are sanitized as follow:
12
+ // - Invalid characters replaced by underscores.
13
+ // - Identifiers longer than 40 chars are truncated (MS-Word limitation).
14
+ expressions.filters.bookmarkCreate = function (input, refid = null) {
15
+ let rand_id = Math.floor(Math.random() * 1000000 + 1000);
16
+ let parsed_id = (refid ? refid : input)
17
+ .replace(/[^a-zA-Z0-9_]/g, '_')
18
+ .substring(0, 40);
19
+
20
+ // Accept both text and OO-XML as input.
21
+ if (input.indexOf('<w:r') !== 0) {
22
+ input = '<w:r><w:t>' + input + '</w:t></w:r>';
23
+ }
24
+
25
+ return (
26
+ '<w:bookmarkStart w:id="' +
27
+ rand_id +
28
+ '" ' +
29
+ 'w:name="' +
30
+ parsed_id +
31
+ '"/>' +
32
+ (refid ? input : '') +
33
+ '<w:bookmarkEnd w:id="' +
34
+ rand_id +
35
+ '"/>'
36
+ );
37
+ };
38
+
39
+ // Creates a hyperlink to a text block or location bookmark:
40
+ // {@input | bookmarkLink: identifier | p}
41
+ // Identifiers are sanitized as follow:
42
+ // - Invalid characters replaced by underscores.
43
+ // - Identifiers longer than 40 chars are truncated (MS-Word limitation).
44
+ expressions.filters.bookmarkLink = function (input, identifier) {
45
+ identifier = identifier.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 40);
46
+ return (
47
+ '<w:hyperlink w:anchor="' +
48
+ identifier +
49
+ '">' +
50
+ '<w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>' +
51
+ '<w:t>' +
52
+ input +
53
+ '</w:t>' +
54
+ '</w:r></w:hyperlink>'
55
+ );
56
+ };
57
+
58
+ // Creates a clickable dynamic field referencing a text block bookmark:
59
+ // {@identifier | bookmarkRef | p}
60
+ // Identifiers are sanitized as follow:
61
+ // - Invalid characters replaced by underscores.
62
+ // - Identifiers longer than 40 chars are truncated (MS-Word limitation).
63
+ expressions.filters.bookmarkRef = function (input) {
64
+ return (
65
+ '<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText xml:space="preserve">' +
66
+ ' REF ' +
67
+ input.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 40) +
68
+ ' \\h </w:instrText></w:r>' +
69
+ '<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>' +
70
+ input +
71
+ '</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>'
72
+ );
73
+ };
74
+
75
+ // Capitalizes input first letter: {input | capfirst}
76
+ expressions.filters.capfirst = function (input) {
77
+ if (!input || input == 'undefined') return input;
78
+ return input.replace(/^\w/, c => c.toUpperCase());
79
+ };
80
+
81
+ // Convert input date with parameter s (full,short): {input | convertDate: 's'}
82
+ expressions.filters.convertDate = function (input, s) {
83
+ var date = new Date(input);
84
+ if (date != 'Invalid Date') {
85
+ var monthsFull = [
86
+ 'January',
87
+ 'February',
88
+ 'March',
89
+ 'April',
90
+ 'May',
91
+ 'June',
92
+ 'July',
93
+ 'August',
94
+ 'September',
95
+ 'October',
96
+ 'November',
97
+ 'December',
98
+ ];
99
+ var monthsShort = [
100
+ '01',
101
+ '02',
102
+ '03',
103
+ '04',
104
+ '05',
105
+ '06',
106
+ '07',
107
+ '08',
108
+ '09',
109
+ '10',
110
+ '11',
111
+ '12',
112
+ ];
113
+ var days = [
114
+ 'Sunday',
115
+ 'Monday',
116
+ 'Tuesday',
117
+ 'Wednesday',
118
+ 'Thursday',
119
+ 'Friday',
120
+ 'Saturday',
121
+ ];
122
+ var day = date.getUTCDate();
123
+ var month = date.getUTCMonth();
124
+ var year = date.getUTCFullYear();
125
+ if (s === 'full') {
126
+ return (
127
+ days[date.getUTCDay()] +
128
+ ', ' +
129
+ monthsFull[month] +
130
+ ' ' +
131
+ (day < 10 ? '0' + day : day) +
132
+ ', ' +
133
+ year
134
+ );
135
+ }
136
+ if (s === 'short') {
137
+ return (
138
+ monthsShort[month] + '/' + (day < 10 ? '0' + day : day) + '/' + year
139
+ );
140
+ }
141
+ }
142
+ };
143
+
144
+ // Convert input date with parameter s (full,short): {input | convertDateLocale: 'locale':'style'}
145
+ expressions.filters.convertDateLocale = function (input, locale, style) {
146
+ var date = new Date(input);
147
+ if (date != 'Invalid Date') {
148
+ var options = { year: 'numeric', month: '2-digit', day: '2-digit' };
149
+
150
+ if (style === 'full')
151
+ options = {
152
+ weekday: 'long',
153
+ year: 'numeric',
154
+ month: 'long',
155
+ day: 'numeric',
156
+ };
157
+
158
+ return date.toLocaleDateString(locale, options);
159
+ }
160
+ };
161
+
162
+ // Convert identifier prefix to a user defined prefix: {identifier | changeID: 'PRJ-'}
163
+ expressions.filters.changeID = function (input, prefix) {
164
+ return input.replace('IDX-', prefix);
165
+ };
166
+
167
+ // Default value: returns input if it is truthy, otherwise its parameter.
168
+ // Example producing a comma-separated list of affected systems, falling-back on the whole audit scope: {affected | lines | d: (scope | select: 'name') | join: ', '}
169
+ expressions.filters.d = function (input, s) {
170
+ return input && input != 'undefined' ? input : s;
171
+ };
172
+
173
+ // Display "From ... to ..." dates nicely, removing redundant information when the start and end date occur during the same month or year: {date_start | fromTo: date_end:'fr' | capfirst}
174
+ // To internationalize or customize the resulting string, associate the desired output to the strings "from {0} to {1}" and "on {0}" in your Pwndoc translate file.
175
+ expressions.filters.fromTo = function (start, end, locale) {
176
+ const start_date = new Date(start);
177
+ const end_date = new Date(end);
178
+ let options = {},
179
+ start_str = '',
180
+ end_str = '';
181
+ let str = 'from {0} to {1}';
182
+
183
+ if (start_date == 'Invalid Date' || end_date == 'Invalid Date') return start;
184
+
185
+ options = { day: '2-digit', month: '2-digit', year: 'numeric' };
186
+ end_str = end_date.toLocaleDateString(locale, options);
187
+
188
+ if (start_date.getYear() != end_date.getYear()) {
189
+ options = { day: '2-digit', month: '2-digit', year: 'numeric' };
190
+ start_str = start_date.toLocaleDateString(locale, options);
191
+ } else if (start_date.getMonth() != end_date.getMonth()) {
192
+ options = { day: '2-digit', month: '2-digit' };
193
+ start_str = start_date.toLocaleDateString(locale, options);
194
+ } else if (start_date.getDay() != end_date.getDay()) {
195
+ options = { day: '2-digit' };
196
+ start_str = start_date.toLocaleDateString(locale, options);
197
+ } else {
198
+ start_str = end_str;
199
+ str = 'on {0}';
200
+ }
201
+
202
+ return translate.translate(str).format(start_str, end_str);
203
+ };
204
+
205
+ // Group input elements by an attribute: {#findings | groupBy: 'severity'}{title}{/findings | groupBy: 'severity'}
206
+ // Source: https://stackoverflow.com/a/34890276
207
+ expressions.filters.groupBy = function (input, key) {
208
+ return expressions.filters.loopObject(
209
+ input.reduce(function (rv, x) {
210
+ (rv[x[key]] = rv[x[key]] || []).push(x);
211
+ return rv;
212
+ }, {}),
213
+ );
214
+ };
215
+
216
+ // Returns the initials from an input string (typically a firstname): {creator.firstname | initials}
217
+ expressions.filters.initials = function (input) {
218
+ if (!input || input == 'undefined') return input;
219
+ return input.replace(/(\w)\w+/gi, '$1.');
220
+ };
221
+
222
+ // Returns a string which is a concatenation of input elements using an optional separator string: {references | join: ', '}
223
+ // Can also be used to build raw OOXML strings.
224
+ expressions.filters.join = function (input, sep = '') {
225
+ if (!input || input == 'undefined') return input;
226
+ return input.join(sep);
227
+ };
228
+
229
+ // Returns the length (ie. number of items for an array) of input: {input | length}
230
+ // Can be used as a conditional to check the emptiness of a list: {#input | length}Not empty{/input | length}
231
+ expressions.filters.length = function (input) {
232
+ return input.length;
233
+ };
234
+
235
+ // Takes a multilines input strings (either raw or simple HTML paragraphs) and returns each line as an ordered list: {input | lines}
236
+ expressions.filters.lines = function (input) {
237
+ if (!input || input == 'undefined') return input;
238
+ if (input.indexOf('<p>') == 0) {
239
+ return input.substring(3, input.length - 4).split('</p><p>');
240
+ } else {
241
+ return input.split('\n');
242
+ }
243
+ };
244
+
245
+ // Creates a hyperlink: {@input | linkTo: 'https://example.com' | p}
246
+ expressions.filters.linkTo = function (input, url) {
247
+ return (
248
+ '<w:r><w:fldChar w:fldCharType="begin"/></w:r>' +
249
+ '<w:r><w:instrText xml:space="preserve"> HYPERLINK "' +
250
+ url +
251
+ '" </w:instrText></w:r>' +
252
+ '<w:r><w:fldChar w:fldCharType="separate"/></w:r>' +
253
+ '<w:r><w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>' +
254
+ '<w:t>' +
255
+ input +
256
+ '</w:t>' +
257
+ '</w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>'
258
+ );
259
+ };
260
+
261
+ // Loop over the input object, providing acccess to its keys and values: {#findings | loopObject}{key}{value.title}{/findings | loopObject}
262
+ // Source: https://stackoverflow.com/a/60887987
263
+ expressions.filters.loopObject = function (input) {
264
+ return Object.keys(input).map(function (key) {
265
+ return { key, value: input[key] };
266
+ });
267
+ };
268
+
269
+ // Lowercases input: {input | lower}
270
+ expressions.filters.lower = function (input) {
271
+ if (!input || input == 'undefined') return input;
272
+ return input.toLowerCase();
273
+ };
274
+
275
+ // Creates a clickable "mailto:" link, assumes that input is an email address if
276
+ // no other address has been provided as parameter:
277
+ // {@lastname | mailto: email | p}
278
+ expressions.filters.mailto = function (input, address = null) {
279
+ return expressions.filters.linkTo(
280
+ input,
281
+ 'mailto:' + (address ? address : input),
282
+ );
283
+ };
284
+
285
+ // Applies a filter on a sequence of objects: {scope | select: 'name' | map: 'lower' | join: ', '}
286
+ expressions.filters.map = function (input, filter) {
287
+ let args = Array.prototype.slice.call(arguments, 2);
288
+ return input.map(x => expressions.filters[filter](x, ...args));
289
+ };
290
+
291
+ // Replace newlines in office XML format: {@input | NewLines}
292
+ expressions.filters.NewLines = function (input) {
293
+ var pre = '<w:p><w:r><w:t>';
294
+ var post = '</w:t></w:r></w:p>';
295
+ var lineBreak = '<w:br/>';
296
+ var result = '';
297
+
298
+ if (!input) return pre + post;
299
+
300
+ input = utils.escapeXMLEntities(input);
301
+ var inputArray = input.split(/\n\n+/g);
302
+ inputArray.forEach(p => {
303
+ result += `${pre}${p.replace(/\n/g, lineBreak)}${post}`;
304
+ });
305
+ // input = input.replace(/\n/g, lineBreak);
306
+ // return pre + input + post;
307
+ return result;
308
+ };
309
+
310
+ // Embeds input within OOXML paragraph tags, applying an optional style name to it: {@input | p: 'Some style'}
311
+ expressions.filters.p = function (input, style = null) {
312
+ let result = '<w:p>';
313
+
314
+ if (style !== null) {
315
+ let style_parsed = style.replaceAll(' ', '');
316
+ result += '<w:pPr><w:pStyle w:val="' + style_parsed + '"/></w:pPr>';
317
+ }
318
+ result += input + '</w:p>';
319
+
320
+ return result;
321
+ };
322
+
323
+ // Reverses the input array: {input | reverse}
324
+ expressions.filters.reverse = function (input) {
325
+ return input.reverse();
326
+ };
327
+
328
+ // Add proper XML tags to embed raw string inside a docxtemplater raw expression: {@('Vulnerability: ' | s) + title | bookmarkCreate: identifier | p}
329
+ expressions.filters.s = function (input) {
330
+ return '<w:r><w:t xml:space="preserve">' + input + '</w:t></w:r>';
331
+ };
332
+
333
+ // Looks up an attribute from a sequence of objects, doted notation is supported: {findings | select: 'cvss.environmentalSeverity'}
334
+ expressions.filters.select = function (input, attr) {
335
+ return input.map(function (item) {
336
+ return _.get(item, attr);
337
+ });
338
+ };
339
+
340
+ // Sorts the input array according an optional given attribute, dotted notation is supported: {#findings | sort 'cvss.environmentalSeverity'}{name}{/findings | sort 'cvss.environmentalSeverity'}
341
+ expressions.filters.sort = function (input, key = null) {
342
+ if (key === null) {
343
+ return input.sort();
344
+ } else {
345
+ return input.sort(function (a, b) {
346
+ return _.get(a, key) < _.get(b, key);
347
+ });
348
+ }
349
+ };
350
+
351
+ // Sort array by supplied field: {#findings | sortArrayByField: 'identifier':1}{/}
352
+ // order: 1 = ascending, -1 = descending
353
+ expressions.filters.sortArrayByField = function (input, field, order) {
354
+ //invalid order sort ascending
355
+ if (order != 1 && order != -1) order = 1;
356
+
357
+ const sorted = input.sort((a, b) => {
358
+ //multiply by order so that if is descending (-1) will reverse the values
359
+ return (
360
+ _.get(a, field).localeCompare(_.get(b, field), undefined, {
361
+ numeric: true,
362
+ }) * order
363
+ );
364
+ });
365
+ return sorted;
366
+ };
367
+
368
+ // Capitalizes input first letter of each word, can be associated to 'lower' to normalize case: {creator.lastname | lower | title}
369
+ expressions.filters.title = function (input) {
370
+ if (!input || input == 'undefined') return input;
371
+ return input.replace(/\w\S*/g, w => w.replace(/^\w/, c => c.toUpperCase()));
372
+ };
373
+
374
+ // Returns the JSON representation of the input value, useful to dump variables content while debugging a template: {input | toJSON}
375
+ expressions.filters.toJSON = function (input) {
376
+ return JSON.stringify(input);
377
+ };
378
+
379
+ // Upercases input: {input | upper}
380
+ expressions.filters.upper = function (input) {
381
+ if (!input || input == 'undefined') return input;
382
+ return input.toUpperCase();
383
+ };
384
+
385
+ // Filters input elements matching a free-form Angular statements: {#findings | where: 'cvss.severity == "Critical"'}{title}{/findings | where: 'cvss.severity == "Critical"'}
386
+ // Source: https://docxtemplater.com/docs/angular-parse/#data-filtering
387
+ expressions.filters.where = function (input, query) {
388
+ return input.filter(function (item) {
389
+ return expressions.compile(query)(item);
390
+ });
391
+ };
392
+
393
+ // Convert HTML data to Open Office XML format: {@input | convertHTML: 'customStyle'}
394
+ expressions.filters.convertHTML = function (input, style) {
395
+ if (typeof input === 'undefined') var result = html2ooxml('');
396
+ else var result = html2ooxml(input.replace(/(<p><\/p>)+$/, ''), style);
397
+ return result;
398
+ };
399
+
400
+ // Count vulnerability by severity
401
+ // Example: {findings | count: 'Critical'}
402
+ expressions.filters.count = function (input, severity) {
403
+ if (!input) return input;
404
+ var count = 0;
405
+
406
+ for (var i = 0; i < input.length; i++) {
407
+ if (input[i].cvss.baseSeverity === severity) {
408
+ count += 1;
409
+ }
410
+ }
411
+
412
+ return count;
413
+ };
414
+
415
+ // Translate using locale from 'translate' folder
416
+ // Example: {input | translate: 'fr'}
417
+ expressions.filters.translate = function (input, locale) {
418
+ translate.setLocale(locale);
419
+ if (!input) return input;
420
+ return translate.translate(input, locale);
421
+ };
422
+
423
+ module.exports = expressions;
backend/src/lib/report-generator.js ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var fs = require('fs');
2
+ var Docxtemplater = require('docxtemplater');
3
+ var PizZip = require('pizzip');
4
+ var expressions = require('./report-filters');
5
+ var ImageModule = require('docxtemplater-image-module-pwndoc');
6
+ var sizeOf = require('image-size');
7
+ var customGenerator = require('./custom-generator');
8
+ var utils = require('./utils');
9
+ var _ = require('lodash');
10
+ var Image = require('mongoose').model('Image');
11
+ const libre = require('libreoffice-convert');
12
+ const { parseAsync } = require('json2csv');
13
+ var Settings = require('mongoose').model('Settings');
14
+ var CVSS31 = require('./cvsscalc31.js');
15
+ var translate = require('../translate');
16
+ var $t;
17
+ const muhammara = require('muhammara');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const { v4: uuidv4 } = require('uuid');
21
+
22
+ // Generate document with docxtemplater
23
+ async function generateDoc(audit) {
24
+ var templatePath = `${__basedir}/../report-templates/${audit.template.name}.${audit.template.ext || 'docx'}`;
25
+ var content = fs.readFileSync(templatePath, 'binary');
26
+
27
+ var zip = new PizZip(content);
28
+
29
+ translate.setLocale(audit.language);
30
+ $t = translate.translate;
31
+
32
+ var settings = await Settings.getAll();
33
+ var preppedAudit = await prepAuditData(audit, settings);
34
+
35
+ var opts = {};
36
+ // opts.centered = true;
37
+ opts.getImage = function (tagValue, tagName) {
38
+ if (tagValue !== 'undefined') {
39
+ tagValue = tagValue.split(',')[1];
40
+ return Buffer.from(tagValue, 'base64');
41
+ }
42
+ // return fs.readFileSync(tagValue, {encoding: 'base64'});
43
+ };
44
+ opts.getSize = function (img, tagValue, tagName) {
45
+ if (img) {
46
+ var sizeObj = sizeOf(img);
47
+ var width = sizeObj.width;
48
+ var height = sizeObj.height;
49
+ if (tagName === 'company.logo_small') {
50
+ var divider = sizeObj.height / 37;
51
+ height = 37;
52
+ width = Math.floor(sizeObj.width / divider);
53
+ } else if (tagName === 'company.logo') {
54
+ var divider = sizeObj.height / 250;
55
+ height = 250;
56
+ width = Math.floor(sizeObj.width / divider);
57
+ if (width > 400) {
58
+ divider = sizeObj.width / 400;
59
+ height = Math.floor(sizeObj.height / divider);
60
+ width = 400;
61
+ }
62
+ } else if (sizeObj.width > 600) {
63
+ var divider = sizeObj.width / 600;
64
+ width = 600;
65
+ height = Math.floor(sizeObj.height / divider);
66
+ }
67
+ return [width, height];
68
+ }
69
+ return [0, 0];
70
+ };
71
+
72
+ if (
73
+ settings.report.private.imageBorder &&
74
+ settings.report.private.imageBorderColor
75
+ )
76
+ opts.border = settings.report.private.imageBorderColor.replace('#', '');
77
+
78
+ try {
79
+ var imageModule = new ImageModule(opts);
80
+ } catch (err) {
81
+ console.log(err);
82
+ }
83
+ var doc = new Docxtemplater()
84
+ .attachModule(imageModule)
85
+ .loadZip(zip)
86
+ .setOptions({ parser: parser, paragraphLoop: true });
87
+ customGenerator.apply(preppedAudit);
88
+ doc.setData(preppedAudit);
89
+ try {
90
+ doc.render();
91
+ } catch (error) {
92
+ if (error.properties.id === 'multi_error') {
93
+ error.properties.errors.forEach(function (err) {
94
+ console.log(err);
95
+ });
96
+ } else console.log(error);
97
+ if (error.properties && error.properties.errors instanceof Array) {
98
+ const errorMessages = error.properties.errors
99
+ .map(function (error) {
100
+ return `Explanation: ${error.properties.explanation}\nScope: ${JSON.stringify(error.properties.scope).substring(0, 142)}...`;
101
+ })
102
+ .join('\n\n');
103
+ // errorMessages is a humanly readable message looking like this :
104
+ // 'The tag beginning with "foobar" is unopened'
105
+ throw `Template Error:\n${errorMessages}`;
106
+ } else {
107
+ throw error;
108
+ }
109
+ }
110
+ var buf = doc.getZip().generate({ type: 'nodebuffer' });
111
+
112
+ return buf;
113
+ }
114
+ exports.generateDoc = generateDoc;
115
+
116
+ // Generates a PDF from a docx using libreoffice-convert
117
+ // libreoffice-convert leverages libreoffice to convert office documents to different formats
118
+ // https://www.npmjs.com/package/libreoffice-convert
119
+ async function generatePdf(audit) {
120
+ var docxReport = await generateDoc(audit);
121
+ return new Promise((resolve, reject) =>
122
+ libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
123
+ if (err) console.log(err);
124
+ resolve(pdf);
125
+ }),
126
+ );
127
+ }
128
+ exports.generatePdf = generatePdf;
129
+
130
+ // Generates a encrypted PDF using libreoffice-convert
131
+ // and muhammara to encrypt it with a given password.
132
+ // https://www.npmjs.com/package/muhammara
133
+
134
+ async function generateEncryptedPdf(audit, password) {
135
+ // Genera el archivo DOCX
136
+ var docxReport = await generateDoc(audit);
137
+
138
+ return new Promise((resolve, reject) => {
139
+ libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
140
+ if (err) {
141
+ console.log(err);
142
+ return reject(err);
143
+ }
144
+
145
+ const tempPdfPath = path.join(
146
+ os.tmpdir(),
147
+ `documento_sin_contraseña_${uuidv4()}.pdf`,
148
+ );
149
+ fs.writeFileSync(tempPdfPath, pdf);
150
+
151
+ const protectedPdfPath = path.join(
152
+ os.tmpdir(),
153
+ `documento_protegido_${uuidv4()}.pdf`,
154
+ );
155
+
156
+ try {
157
+ const Recipe = muhammara.Recipe;
158
+ const pdfDoc = new Recipe(tempPdfPath, protectedPdfPath);
159
+
160
+ pdfDoc
161
+ .encrypt({
162
+ userPassword: password,
163
+ ownerPassword: password,
164
+ userProtectionFlag: 4,
165
+ })
166
+ .endPDF();
167
+
168
+ const protectedPdf = fs.readFileSync(protectedPdfPath);
169
+
170
+ fs.unlinkSync(tempPdfPath);
171
+ fs.unlinkSync(protectedPdfPath);
172
+
173
+ resolve(protectedPdf);
174
+ } catch (error) {
175
+ console.error('Error protecting PDF:', error);
176
+ reject(error);
177
+ }
178
+ });
179
+ });
180
+ }
181
+ exports.generateEncryptedPdf = generateEncryptedPdf;
182
+
183
+ // Generates a csv from the json data
184
+ // Leverages json2csv
185
+ // https://www.npmjs.com/package/json2csv
186
+ async function generateCsv(audit) {
187
+ return parseAsync(audit._doc);
188
+ }
189
+ exports.generateCsv = generateCsv;
190
+
191
+ // Filters helper: handles the use of preformated easilly translatable strings.
192
+ // Source: https://www.tutorialstonight.com/javascript-string-format.php
193
+ String.prototype.format = function () {
194
+ let args = arguments;
195
+ return this.replace(/{([0-9]+)}/g, function (match, index) {
196
+ return typeof args[index] == 'undefined' ? match : args[index];
197
+ });
198
+ };
199
+
200
+ // Compile all angular expressions
201
+ var angularParser = function (tag) {
202
+ expressions = { ...expressions, ...customGenerator.expressions };
203
+ if (tag === '.') {
204
+ return {
205
+ get: function (s) {
206
+ return s;
207
+ },
208
+ };
209
+ }
210
+ const expr = expressions.compile(
211
+ tag.replace(/(’|‘)/g, "'").replace(/(“|”)/g, '"'),
212
+ );
213
+ return {
214
+ get: function (scope, context) {
215
+ let obj = {};
216
+ const scopeList = context.scopeList;
217
+ const num = context.num;
218
+ for (let i = 0, len = num + 1; i < len; i++) {
219
+ obj = _.merge(obj, scopeList[i]);
220
+ }
221
+ return expr(scope, obj);
222
+ },
223
+ };
224
+ };
225
+
226
+ function parser(tag) {
227
+ // We write an exception to handle the tag "$pageBreakExceptLast"
228
+ if (tag === '$pageBreakExceptLast') {
229
+ return {
230
+ get(scope, context) {
231
+ const totalLength =
232
+ context.scopePathLength[context.scopePathLength.length - 1];
233
+ const index = context.scopePathItem[context.scopePathItem.length - 1];
234
+ const isLast = index === totalLength - 1;
235
+ if (!isLast) {
236
+ return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>';
237
+ } else {
238
+ return '';
239
+ }
240
+ },
241
+ };
242
+ }
243
+ // We use the angularParser as the default fallback
244
+ // If you don't wish to use the angularParser,
245
+ // you can use the default parser as documented here:
246
+ // https://docxtemplater.readthedocs.io/en/latest/configuration.html#default-parser
247
+ return angularParser(tag);
248
+ }
249
+ function cvssStrToObject(cvss) {
250
+ var initialState = 'Not Defined';
251
+ var res = {
252
+ AV: initialState,
253
+ AC: initialState,
254
+ PR: initialState,
255
+ UI: initialState,
256
+ S: initialState,
257
+ C: initialState,
258
+ I: initialState,
259
+ A: initialState,
260
+ E: initialState,
261
+ RL: initialState,
262
+ RC: initialState,
263
+ CR: initialState,
264
+ IR: initialState,
265
+ AR: initialState,
266
+ MAV: initialState,
267
+ MAC: initialState,
268
+ MPR: initialState,
269
+ MUI: initialState,
270
+ MS: initialState,
271
+ MC: initialState,
272
+ MI: initialState,
273
+ MA: initialState,
274
+ };
275
+ if (cvss) {
276
+ var temp = cvss.split('/');
277
+ for (var i = 0; i < temp.length; i++) {
278
+ var elt = temp[i].split(':');
279
+ switch (elt[0]) {
280
+ case 'AV':
281
+ if (elt[1] === 'N') res.AV = 'Network';
282
+ else if (elt[1] === 'A') res.AV = 'Adjacent Network';
283
+ else if (elt[1] === 'L') res.AV = 'Local';
284
+ else if (elt[1] === 'P') res.AV = 'Physical';
285
+ res.AV = $t(res.AV);
286
+ break;
287
+ case 'AC':
288
+ if (elt[1] === 'L') res.AC = 'Low';
289
+ else if (elt[1] === 'H') res.AC = 'High';
290
+ res.AC = $t(res.AC);
291
+ break;
292
+ case 'PR':
293
+ if (elt[1] === 'N') res.PR = 'None';
294
+ else if (elt[1] === 'L') res.PR = 'Low';
295
+ else if (elt[1] === 'H') res.PR = 'High';
296
+ res.PR = $t(res.PR);
297
+ break;
298
+ case 'UI':
299
+ if (elt[1] === 'N') res.UI = 'None';
300
+ else if (elt[1] === 'R') res.UI = 'Required';
301
+ res.UI = $t(res.UI);
302
+ break;
303
+ case 'S':
304
+ if (elt[1] === 'U') res.S = 'Unchanged';
305
+ else if (elt[1] === 'C') res.S = 'Changed';
306
+ res.S = $t(res.S);
307
+ break;
308
+ case 'C':
309
+ if (elt[1] === 'N') res.C = 'None';
310
+ else if (elt[1] === 'L') res.C = 'Low';
311
+ else if (elt[1] === 'H') res.C = 'High';
312
+ res.C = $t(res.C);
313
+ break;
314
+ case 'I':
315
+ if (elt[1] === 'N') res.I = 'None';
316
+ else if (elt[1] === 'L') res.I = 'Low';
317
+ else if (elt[1] === 'H') res.I = 'High';
318
+ res.I = $t(res.I);
319
+ break;
320
+ case 'A':
321
+ if (elt[1] === 'N') res.A = 'None';
322
+ else if (elt[1] === 'L') res.A = 'Low';
323
+ else if (elt[1] === 'H') res.A = 'High';
324
+ res.A = $t(res.A);
325
+ break;
326
+ case 'E':
327
+ if (elt[1] === 'U') res.E = 'Unproven';
328
+ else if (elt[1] === 'P') res.E = 'Proof-of-Concept';
329
+ else if (elt[1] === 'F') res.E = 'Functional';
330
+ else if (elt[1] === 'H') res.E = 'High';
331
+ res.E = $t(res.E);
332
+ break;
333
+ case 'RL':
334
+ if (elt[1] === 'O') res.RL = 'Official Fix';
335
+ else if (elt[1] === 'T') res.RL = 'Temporary Fix';
336
+ else if (elt[1] === 'W') res.RL = 'Workaround';
337
+ else if (elt[1] === 'U') res.RL = 'Unavailable';
338
+ res.RL = $t(res.RL);
339
+ break;
340
+ case 'RC':
341
+ if (elt[1] === 'U') res.RC = 'Unknown';
342
+ else if (elt[1] === 'R') res.RC = 'Reasonable';
343
+ else if (elt[1] === 'C') res.RC = 'Confirmed';
344
+ res.RC = $t(res.RC);
345
+ break;
346
+ case 'CR':
347
+ if (elt[1] === 'L') res.CR = 'Low';
348
+ else if (elt[1] === 'M') res.CR = 'Medium';
349
+ else if (elt[1] === 'H') res.CR = 'High';
350
+ res.CR = $t(res.CR);
351
+ break;
352
+ case 'IR':
353
+ if (elt[1] === 'L') res.IR = 'Low';
354
+ else if (elt[1] === 'M') res.IR = 'Medium';
355
+ else if (elt[1] === 'H') res.IR = 'High';
356
+ res.IR = $t(res.IR);
357
+ break;
358
+ case 'AR':
359
+ if (elt[1] === 'L') res.AR = 'Low';
360
+ else if (elt[1] === 'M') res.AR = 'Medium';
361
+ else if (elt[1] === 'H') res.AR = 'High';
362
+ res.AR = $t(res.AR);
363
+ break;
364
+ case 'MAV':
365
+ if (elt[1] === 'N') res.MAV = 'Network';
366
+ else if (elt[1] === 'A') res.MAV = 'Adjacent Network';
367
+ else if (elt[1] === 'L') res.MAV = 'Local';
368
+ else if (elt[1] === 'P') res.MAV = 'Physical';
369
+ res.MAV = $t(res.MAV);
370
+ break;
371
+ case 'MAC':
372
+ if (elt[1] === 'L') res.MAC = 'Low';
373
+ else if (elt[1] === 'H') res.MAC = 'High';
374
+ res.MAC = $t(res.MAC);
375
+ break;
376
+ case 'MPR':
377
+ if (elt[1] === 'N') res.MPR = 'None';
378
+ else if (elt[1] === 'L') res.MPR = 'Low';
379
+ else if (elt[1] === 'H') res.MPR = 'High';
380
+ res.MPR = $t(res.MPR);
381
+ break;
382
+ case 'MUI':
383
+ if (elt[1] === 'N') res.MUI = 'None';
384
+ else if (elt[1] === 'R') res.MUI = 'Required';
385
+ res.MUI = $t(res.MUI);
386
+ break;
387
+ case 'MS':
388
+ if (elt[1] === 'U') res.MS = 'Unchanged';
389
+ else if (elt[1] === 'C') res.MS = 'Changed';
390
+ res.MS = $t(res.MS);
391
+ break;
392
+ case 'MC':
393
+ if (elt[1] === 'N') res.MC = 'None';
394
+ else if (elt[1] === 'L') res.MC = 'Low';
395
+ else if (elt[1] === 'H') res.MC = 'High';
396
+ res.MC = $t(res.MC);
397
+ break;
398
+ case 'MI':
399
+ if (elt[1] === 'N') res.MI = 'None';
400
+ else if (elt[1] === 'L') res.MI = 'Low';
401
+ else if (elt[1] === 'H') res.MI = 'High';
402
+ res.MI = $t(res.MI);
403
+ break;
404
+ case 'MA':
405
+ if (elt[1] === 'N') res.MA = 'None';
406
+ else if (elt[1] === 'L') res.MA = 'Low';
407
+ else if (elt[1] === 'H') res.MA = 'High';
408
+ res.MA = $t(res.MA);
409
+ break;
410
+ default:
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ return res;
416
+ }
417
+
418
+ async function prepAuditData(data, settings) {
419
+ /** CVSS Colors for table cells */
420
+ var noneColor = settings.report.public.cvssColors.noneColor.replace('#', ''); //default of blue ("#4A86E8")
421
+ var lowColor = settings.report.public.cvssColors.lowColor.replace('#', ''); //default of green ("#008000")
422
+ var mediumColor = settings.report.public.cvssColors.mediumColor.replace(
423
+ '#',
424
+ '',
425
+ ); //default of yellow ("#f9a009")
426
+ var highColor = settings.report.public.cvssColors.highColor.replace('#', ''); //default of red ("#fe0000")
427
+ var criticalColor = settings.report.public.cvssColors.criticalColor.replace(
428
+ '#',
429
+ '',
430
+ ); //default of black ("#212121")
431
+
432
+ var cellNoneColor =
433
+ '<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
434
+ noneColor +
435
+ '"/></w:tcPr>';
436
+ var cellLowColor =
437
+ '<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
438
+ lowColor +
439
+ '"/></w:tcPr>';
440
+ var cellMediumColor =
441
+ '<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
442
+ mediumColor +
443
+ '"/></w:tcPr>';
444
+ var cellHighColor =
445
+ '<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
446
+ highColor +
447
+ '"/></w:tcPr>';
448
+ var cellCriticalColor =
449
+ '<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="' +
450
+ criticalColor +
451
+ '"/></w:tcPr>';
452
+
453
+ var result = {};
454
+ result.name = data.name || 'undefined';
455
+ result.auditType = $t(data.auditType) || 'undefined';
456
+ result.date = data.date || 'undefined';
457
+ result.date_start = data.date_start || 'undefined';
458
+ result.date_end = data.date_end || 'undefined';
459
+ if (data.customFields) {
460
+ for (var field of data.customFields) {
461
+ var fieldType = field.customField.fieldType;
462
+ var label = field.customField.label;
463
+
464
+ if (fieldType === 'text')
465
+ result[_.deburr(label.toLowerCase()).replace(/\s/g, '')] =
466
+ await splitHTMLParagraphs(field.text);
467
+ else if (fieldType !== 'space')
468
+ result[_.deburr(label.toLowerCase()).replace(/\s/g, '')] = field.text;
469
+ }
470
+ }
471
+
472
+ result.company = {};
473
+ if (data.company) {
474
+ result.company.name = data.company.name || 'undefined';
475
+ result.company.shortName = data.company.shortName || result.company.name;
476
+ result.company.logo = data.company.logo || 'undefined';
477
+ result.company.logo_small = data.company.logo || 'undefined';
478
+ }
479
+
480
+ result.client = {};
481
+ if (data.client) {
482
+ result.client.email = data.client.email || 'undefined';
483
+ result.client.firstname = data.client.firstname || 'undefined';
484
+ result.client.lastname = data.client.lastname || 'undefined';
485
+ result.client.phone = data.client.phone || 'undefined';
486
+ result.client.cell = data.client.cell || 'undefined';
487
+ result.client.title = data.client.title || 'undefined';
488
+ }
489
+
490
+ result.collaborators = [];
491
+ data.collaborators.forEach(collab => {
492
+ result.collaborators.push({
493
+ username: collab.username || 'undefined',
494
+ firstname: collab.firstname || 'undefined',
495
+ lastname: collab.lastname || 'undefined',
496
+ email: collab.email || 'undefined',
497
+ phone: collab.phone || 'undefined',
498
+ role: collab.role || 'undefined',
499
+ });
500
+ });
501
+ result.language = data.language || 'undefined';
502
+ result.scope = data.scope.toObject() || [];
503
+
504
+ result.findings = [];
505
+ for (var finding of data.findings) {
506
+ var tmpCVSS = CVSS31.calculateCVSSFromVector(finding.cvssv3);
507
+ var tmpFinding = {
508
+ title: finding.title || '',
509
+ vulnType: $t(finding.vulnType) || '',
510
+ description: await splitHTMLParagraphs(finding.description),
511
+ observation: await splitHTMLParagraphs(finding.observation),
512
+ remediation: await splitHTMLParagraphs(finding.remediation),
513
+ remediationComplexity: finding.remediationComplexity || '',
514
+ priority: finding.priority || '',
515
+ references: finding.references || [],
516
+ cwes: finding.cwes || [],
517
+ poc: await splitHTMLParagraphs(finding.poc),
518
+ affected: finding.scope || '',
519
+ status: finding.status || '',
520
+ category: $t(finding.category) || $t('No Category'),
521
+ identifier: 'IDX-' + utils.lPad(finding.identifier),
522
+ retestStatus: finding?.retestStatus ? $t(finding.retestStatus) : '',
523
+ retestDescription: await splitHTMLParagraphs(finding.retestDescription),
524
+ };
525
+ // Handle CVSS
526
+ tmpFinding.cvss = {
527
+ vectorString: tmpCVSS.vectorString || '',
528
+ baseMetricScore: tmpCVSS.baseMetricScore || '',
529
+ baseSeverity: tmpCVSS.baseSeverity || '',
530
+ temporalMetricScore: tmpCVSS.temporalMetricScore || '',
531
+ temporalSeverity: tmpCVSS.temporalSeverity || '',
532
+ environmentalMetricScore: tmpCVSS.environmentalMetricScore || '',
533
+ environmentalSeverity: tmpCVSS.environmentalSeverity || '',
534
+ };
535
+ if (tmpCVSS.baseImpact)
536
+ tmpFinding.cvss.baseImpact = CVSS31.roundUp1(tmpCVSS.baseImpact);
537
+ else tmpFinding.cvss.baseImpact = '';
538
+ if (tmpCVSS.baseExploitability)
539
+ tmpFinding.cvss.baseExploitability = CVSS31.roundUp1(
540
+ tmpCVSS.baseExploitability,
541
+ );
542
+ else tmpFinding.cvss.baseExploitability = '';
543
+
544
+ if (tmpCVSS.environmentalModifiedImpact)
545
+ tmpFinding.cvss.environmentalModifiedImpact = CVSS31.roundUp1(
546
+ tmpCVSS.environmentalModifiedImpact,
547
+ );
548
+ else tmpFinding.cvss.environmentalModifiedImpact = '';
549
+ if (tmpCVSS.environmentalModifiedExploitability)
550
+ tmpFinding.cvss.environmentalModifiedExploitability = CVSS31.roundUp1(
551
+ tmpCVSS.environmentalModifiedExploitability,
552
+ );
553
+ else tmpFinding.cvss.environmentalModifiedExploitability = '';
554
+
555
+ if (tmpCVSS.baseSeverity === 'Low')
556
+ tmpFinding.cvss.cellColor = cellLowColor;
557
+ else if (tmpCVSS.baseSeverity === 'Medium')
558
+ tmpFinding.cvss.cellColor = cellMediumColor;
559
+ else if (tmpCVSS.baseSeverity === 'High')
560
+ tmpFinding.cvss.cellColor = cellHighColor;
561
+ else if (tmpCVSS.baseSeverity === 'Critical')
562
+ tmpFinding.cvss.cellColor = cellCriticalColor;
563
+ else tmpFinding.cvss.cellColor = cellNoneColor;
564
+
565
+ if (tmpCVSS.temporalSeverity === 'Low')
566
+ tmpFinding.cvss.temporalCellColor = cellLowColor;
567
+ else if (tmpCVSS.temporalSeverity === 'Medium')
568
+ tmpFinding.cvss.temporalCellColor = cellMediumColor;
569
+ else if (tmpCVSS.temporalSeverity === 'High')
570
+ tmpFinding.cvss.temporalCellColor = cellHighColor;
571
+ else if (tmpCVSS.temporalSeverity === 'Critical')
572
+ tmpFinding.cvss.temporalCellColor = cellCriticalColor;
573
+ else tmpFinding.cvss.temporalCellColor = cellNoneColor;
574
+
575
+ if (tmpCVSS.environmentalSeverity === 'Low')
576
+ tmpFinding.cvss.environmentalCellColor = cellLowColor;
577
+ else if (tmpCVSS.environmentalSeverity === 'Medium')
578
+ tmpFinding.cvss.environmentalCellColor = cellMediumColor;
579
+ else if (tmpCVSS.environmentalSeverity === 'High')
580
+ tmpFinding.cvss.environmentalCellColor = cellHighColor;
581
+ else if (tmpCVSS.environmentalSeverity === 'Critical')
582
+ tmpFinding.cvss.environmentalCellColor = cellCriticalColor;
583
+ else tmpFinding.cvss.environmentalCellColor = cellNoneColor;
584
+
585
+ tmpFinding.cvssObj = cvssStrToObject(tmpCVSS.vectorString);
586
+
587
+ if (finding.customFields) {
588
+ for (field of finding.customFields) {
589
+ // For retrocompatibility of findings with old customFields
590
+ // or if custom field has been deleted, last saved custom fields will be available
591
+ if (field.customField) {
592
+ var fieldType = field.customField.fieldType;
593
+ var label = field.customField.label;
594
+ } else {
595
+ var fieldType = field.fieldType;
596
+ var label = field.label;
597
+ }
598
+ if (fieldType === 'text')
599
+ tmpFinding[
600
+ _.deburr(label.toLowerCase())
601
+ .replace(/\s/g, '')
602
+ .replace(/[^\w]/g, '_')
603
+ ] = await splitHTMLParagraphs(field.text);
604
+ else if (fieldType !== 'space')
605
+ tmpFinding[
606
+ _.deburr(label.toLowerCase())
607
+ .replace(/\s/g, '')
608
+ .replace(/[^\w]/g, '_')
609
+ ] = field.text;
610
+ }
611
+ }
612
+ result.findings.push(tmpFinding);
613
+ }
614
+
615
+ result.categories = _.chain(result.findings)
616
+ .groupBy('category')
617
+ .map((value, key) => {
618
+ return { categoryName: key, categoryFindings: value };
619
+ })
620
+ .value();
621
+
622
+ result.creator = {};
623
+ if (data.creator) {
624
+ result.creator.username = data.creator.username || 'undefined';
625
+ result.creator.firstname = data.creator.firstname || 'undefined';
626
+ result.creator.lastname = data.creator.lastname || 'undefined';
627
+ result.creator.email = data.creator.email || 'undefined';
628
+ result.creator.phone = data.creator.phone || 'undefined';
629
+ result.creator.role = data.creator.role || 'undefined';
630
+ }
631
+
632
+ for (var section of data.sections) {
633
+ var formatSection = {
634
+ name: $t(section.name),
635
+ };
636
+ if (section.text)
637
+ // keep text for retrocompatibility
638
+ formatSection.text = await splitHTMLParagraphs(section.text);
639
+ else if (section.customFields) {
640
+ for (field of section.customFields) {
641
+ var fieldType = field.customField.fieldType;
642
+ var label = field.customField.label;
643
+ if (fieldType === 'text')
644
+ formatSection[
645
+ _.deburr(label.toLowerCase())
646
+ .replace(/\s/g, '')
647
+ .replace(/[^\w]/g, '_')
648
+ ] = await splitHTMLParagraphs(field.text);
649
+ else if (fieldType !== 'space')
650
+ formatSection[
651
+ _.deburr(label.toLowerCase())
652
+ .replace(/\s/g, '')
653
+ .replace(/[^\w]/g, '_')
654
+ ] = field.text;
655
+ }
656
+ }
657
+ result[section.field] = formatSection;
658
+ }
659
+ replaceSubTemplating(result);
660
+ return result;
661
+ }
662
+
663
+ async function splitHTMLParagraphs(data) {
664
+ var result = [];
665
+ if (!data) return result;
666
+
667
+ var splitted = data.split(/(<img.+?src=".*?".+?alt=".*?".*?>)/);
668
+
669
+ for (var value of splitted) {
670
+ if (value.startsWith('<img')) {
671
+ var src = value.match(/<img.+src="(.*?)"/) || '';
672
+ var alt = value.match(/<img.+alt="(.*?)"/) || '';
673
+ if (src && src.length > 1) src = src[1];
674
+ if (alt && alt.length > 1) alt = _.unescape(alt[1]);
675
+
676
+ if (!src.startsWith('data')) {
677
+ try {
678
+ src = (await Image.getOne(src)).value;
679
+ } catch (error) {
680
+ src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=';
681
+ }
682
+ }
683
+ if (result.length === 0) result.push({ text: '', images: [] });
684
+ result[result.length - 1].images.push({ image: src, caption: alt });
685
+ } else if (value === '') {
686
+ continue;
687
+ } else {
688
+ result.push({ text: value, images: [] });
689
+ }
690
+ }
691
+ return result;
692
+ }
693
+
694
+ function replaceSubTemplating(o, originalData = o) {
695
+ var regexp = /\{_\{([a-zA-Z0-9\[\]\_\.]{1,})\}_\}/gm;
696
+ if (Array.isArray(o))
697
+ o.forEach(key => replaceSubTemplating(key, originalData));
698
+ else if (typeof o === 'object' && !!o) {
699
+ Object.keys(o).forEach(key => {
700
+ if (typeof o[key] === 'string')
701
+ o[key] = o[key].replace(regexp, (match, word) =>
702
+ _.get(originalData, word.trim(), ''),
703
+ );
704
+ else replaceSubTemplating(o[key], originalData);
705
+ });
706
+ }
707
+ }
backend/src/lib/utils.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Filename whitelist validation for template creation
2
+ function validFilename(filename) {
3
+ const regex = /^[\p{Letter}\p{Mark}0-9 \[\]'()_-]+$/iu;
4
+
5
+ return regex.test(filename);
6
+ }
7
+ exports.validFilename = validFilename;
8
+
9
+ // Escape XML special entities when using {@RawXML} in template generation
10
+ function escapeXMLEntities(input) {
11
+ var XML_CHAR_MAP = { '<': '&lt;', '>': '&gt;', '&': '&amp;' };
12
+ var standardEncode = input.replace(/[<>&]/g, function (ch) {
13
+ return XML_CHAR_MAP[ch];
14
+ });
15
+ return standardEncode;
16
+ }
17
+ exports.escapeXMLEntities = escapeXMLEntities;
18
+
19
+ // Convert number to 3 digits format if under 100
20
+ function lPad(number) {
21
+ if (number <= 99) {
22
+ number = ('00' + number).slice(-3);
23
+ }
24
+ return `${number}`;
25
+ }
26
+ exports.lPad = lPad;
27
+
28
+ function escapeRegex(regex) {
29
+ return regex.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
30
+ }
31
+ exports.escapeRegex = escapeRegex;
32
+
33
+ function generateUUID() {
34
+ return require('crypto').randomBytes(32).toString('hex');
35
+ }
36
+ exports.generateUUID = generateUUID;
37
+
38
+ var getObjectPaths = (obj, prefix = '') =>
39
+ Object.keys(obj).reduce((res, el) => {
40
+ if (Array.isArray(obj[el])) {
41
+ return [...res, prefix + el];
42
+ } else if (typeof obj[el] === 'object' && obj[el] !== null) {
43
+ return [...res, ...getObjectPaths(obj[el], prefix + el + '.')];
44
+ }
45
+ return [...res, prefix + el];
46
+ }, []);
47
+ exports.getObjectPaths = getObjectPaths;
48
+
49
+ function getSockets(io, room) {
50
+ var result = [];
51
+ io.sockets.sockets.forEach(data => {
52
+ if (data.rooms.has(room)) {
53
+ result.push(data);
54
+ }
55
+ });
56
+ return result;
57
+ }
58
+ exports.getSockets = getSockets;
backend/src/models/audit-type.js ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var Template = {
5
+ _id: false,
6
+ template: { type: Schema.Types.ObjectId, ref: 'Template' },
7
+ locale: String,
8
+ };
9
+
10
+ var AuditTypeSchema = new Schema(
11
+ {
12
+ name: { type: String, unique: true },
13
+ templates: [Template],
14
+ sections: [{ type: String, ref: 'CustomSection' }],
15
+ hidden: [{ type: String, enum: ['network', 'findings'] }],
16
+ stage: {
17
+ type: String,
18
+ enum: ['default', 'retest', 'multi'],
19
+ default: 'default',
20
+ },
21
+ },
22
+ { timestamps: true },
23
+ );
24
+
25
+ /*
26
+ *** Statics ***
27
+ */
28
+
29
+ // Get all auditTypes
30
+ AuditTypeSchema.statics.getAll = () => {
31
+ return new Promise((resolve, reject) => {
32
+ var query = AuditType.find();
33
+ query.select('_id name templates sections hidden stage');
34
+ query
35
+ .exec()
36
+ .then(rows => {
37
+ resolve(rows);
38
+ })
39
+ .catch(err => {
40
+ reject(err);
41
+ });
42
+ });
43
+ };
44
+
45
+ // Get auditType by name
46
+ AuditTypeSchema.statics.getByName = name => {
47
+ return new Promise((resolve, reject) => {
48
+ var query = AuditType.findOne({ name: name });
49
+ query.select('-_id name templates sections hidden stage');
50
+ query
51
+ .exec()
52
+ .then(rows => {
53
+ resolve(rows);
54
+ })
55
+ .catch(err => {
56
+ reject(err);
57
+ });
58
+ });
59
+ };
60
+
61
+ // Create auditType
62
+ AuditTypeSchema.statics.create = auditType => {
63
+ return new Promise((resolve, reject) => {
64
+ var query = new AuditType(auditType);
65
+ query
66
+ .save()
67
+ .then(row => {
68
+ resolve(row);
69
+ })
70
+ .catch(err => {
71
+ if (err.code === 11000)
72
+ reject({ fn: 'BadParameters', message: 'Audit Type already exists' });
73
+ else reject(err);
74
+ });
75
+ });
76
+ };
77
+
78
+ // Update Audit Types
79
+ AuditTypeSchema.statics.updateAll = auditTypes => {
80
+ return new Promise((resolve, reject) => {
81
+ AuditType.deleteMany()
82
+ .then(row => {
83
+ AuditType.insertMany(auditTypes);
84
+ })
85
+ .then(row => {
86
+ resolve('Audit Types updated successfully');
87
+ })
88
+ .catch(err => {
89
+ reject(err);
90
+ });
91
+ });
92
+ };
93
+
94
+ // Delete auditType
95
+ AuditTypeSchema.statics.delete = name => {
96
+ return new Promise((resolve, reject) => {
97
+ AuditType.deleteOne({ name: name })
98
+ .then(res => {
99
+ if (res.deletedCount === 1) resolve('Audit Type deleted');
100
+ else reject({ fn: 'NotFound', message: 'Audit Type not found' });
101
+ })
102
+ .catch(err => {
103
+ reject(err);
104
+ });
105
+ });
106
+ };
107
+
108
+ /*
109
+ *** Methods ***
110
+ */
111
+
112
+ var AuditType = mongoose.model('AuditType', AuditTypeSchema);
113
+ AuditType.syncIndexes();
114
+ module.exports = AuditType;
backend/src/models/audit.js ADDED
@@ -0,0 +1,1195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose'); //.set('debug', true);
2
+ const CVSS31 = require('../lib/cvsscalc31');
3
+ var Schema = mongoose.Schema;
4
+
5
+ var Paragraph = {
6
+ text: String,
7
+ images: [{ image: String, caption: String }],
8
+ };
9
+
10
+ var customField = {
11
+ _id: false,
12
+ customField: { type: Schema.Types.Mixed, ref: 'CustomField' },
13
+ text: Schema.Types.Mixed,
14
+ };
15
+
16
+ var Finding = {
17
+ id: Schema.Types.ObjectId,
18
+ identifier: Number, //incremental ID to be shown in the report
19
+ title: String,
20
+ vulnType: String,
21
+ description: String,
22
+ observation: String,
23
+ remediation: String,
24
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
25
+ priority: { type: Number, enum: [1, 2, 3, 4] },
26
+ references: [String],
27
+ cwes: [String],
28
+ cvssv3: String,
29
+ paragraphs: [Paragraph],
30
+ poc: String,
31
+ scope: String,
32
+ status: { type: Number, enum: [0, 1], default: 1 }, // 0: done, 1: redacting
33
+ category: String,
34
+ customFields: [customField],
35
+ retestStatus: { type: String, enum: ['ok', 'ko', 'unknown', 'partial'] },
36
+ retestDescription: String,
37
+ };
38
+
39
+ var Service = {
40
+ port: Number,
41
+ protocol: { type: String, enum: ['tcp', 'udp'] },
42
+ name: String,
43
+ product: String,
44
+ version: String,
45
+ };
46
+
47
+ var Host = {
48
+ hostname: String,
49
+ ip: String,
50
+ os: String,
51
+ services: [Service],
52
+ };
53
+
54
+ var SortOption = {
55
+ _id: false,
56
+ category: String,
57
+ sortValue: String,
58
+ sortOrder: { type: String, enum: ['desc', 'asc'] },
59
+ sortAuto: Boolean,
60
+ };
61
+
62
+ var AuditSchema = new Schema(
63
+ {
64
+ name: { type: String, required: true },
65
+ auditType: String,
66
+ date: String,
67
+ date_start: String,
68
+ date_end: String,
69
+ summary: String,
70
+ company: { type: Schema.Types.ObjectId, ref: 'Company' },
71
+ client: { type: Schema.Types.ObjectId, ref: 'Client' },
72
+ collaborators: [{ type: Schema.Types.ObjectId, ref: 'User' }],
73
+ reviewers: [{ type: Schema.Types.ObjectId, ref: 'User' }],
74
+ language: { type: String, required: true },
75
+ scope: [{ _id: false, name: String, hosts: [Host] }],
76
+ findings: [Finding],
77
+ template: { type: Schema.Types.ObjectId, ref: 'Template' },
78
+ creator: { type: Schema.Types.ObjectId, ref: 'User' },
79
+ sections: [
80
+ {
81
+ field: String,
82
+ name: String,
83
+ text: String,
84
+ customFields: [customField],
85
+ },
86
+ ], // keep text for retrocompatibility
87
+ customFields: [customField],
88
+ sortFindings: [SortOption],
89
+ state: {
90
+ type: String,
91
+ enum: ['EDIT', 'REVIEW', 'APPROVED'],
92
+ default: 'EDIT',
93
+ },
94
+ approvals: [{ type: Schema.Types.ObjectId, ref: 'User' }],
95
+ type: {
96
+ type: String,
97
+ enum: ['default', 'multi', 'retest'],
98
+ default: 'default',
99
+ },
100
+ parentId: { type: Schema.Types.ObjectId, ref: 'Audit' },
101
+ },
102
+ { timestamps: true },
103
+ );
104
+
105
+ /*
106
+ *** Statics ***
107
+ */
108
+
109
+ // Get all audits (admin)
110
+ AuditSchema.statics.getAudits = (isAdmin, userId, filters) => {
111
+ return new Promise((resolve, reject) => {
112
+ var query = Audit.find(filters);
113
+ if (!isAdmin)
114
+ query.or([
115
+ { creator: userId },
116
+ { collaborators: userId },
117
+ { reviewers: userId },
118
+ ]);
119
+ query.populate('creator', 'username');
120
+ query.populate('collaborators', 'username');
121
+ query.populate('reviewers', 'username firstname lastname');
122
+ query.populate('approvals', 'username firstname lastname');
123
+ query.populate('company', 'name');
124
+ query.populate('template', '-_id ext');
125
+ query.select(
126
+ 'id name auditType language creator collaborators company createdAt state type parentId template',
127
+ );
128
+ query
129
+ .exec()
130
+ .then(rows => {
131
+ resolve(rows);
132
+ })
133
+ .catch(err => {
134
+ reject(err);
135
+ });
136
+ });
137
+ };
138
+
139
+ // Get Audit with ID to generate report
140
+ AuditSchema.statics.getAudit = (isAdmin, auditId, userId) => {
141
+ return new Promise((resolve, reject) => {
142
+ var query = Audit.findById(auditId);
143
+ if (!isAdmin)
144
+ query.or([
145
+ { creator: userId },
146
+ { collaborators: userId },
147
+ { reviewers: userId },
148
+ ]);
149
+ query.populate('template');
150
+ query.populate('creator', 'username firstname lastname email phone role');
151
+ query.populate('company');
152
+ query.populate('client');
153
+ query.populate(
154
+ 'collaborators',
155
+ 'username firstname lastname email phone role',
156
+ );
157
+ query.populate('reviewers', 'username firstname lastname role');
158
+ query.populate('approvals', 'username firstname lastname role');
159
+ query.populate('customFields.customField', 'label fieldType text');
160
+ query.populate({
161
+ path: 'findings',
162
+ populate: {
163
+ path: 'customFields.customField',
164
+ select: 'label fieldType text',
165
+ },
166
+ });
167
+ query
168
+ .exec()
169
+ .then(row => {
170
+ if (!row)
171
+ throw {
172
+ fn: 'NotFound',
173
+ message: 'Audit not found or Insufficient Privileges',
174
+ };
175
+ resolve(row);
176
+ })
177
+ .catch(err => {
178
+ if (err.name === 'CastError')
179
+ reject({ fn: 'BadParameters', message: 'Bad Audit Id' });
180
+ else reject(err);
181
+ });
182
+ });
183
+ };
184
+
185
+ AuditSchema.statics.getAuditChildren = (isAdmin, auditId, userId) => {
186
+ return new Promise((resolve, reject) => {
187
+ var query = Audit.find({ parentId: auditId });
188
+ if (!isAdmin)
189
+ query.or([
190
+ { creator: userId },
191
+ { collaborators: userId },
192
+ { reviewers: userId },
193
+ ]);
194
+ query
195
+ .exec()
196
+ .then(rows => {
197
+ if (!rows)
198
+ throw {
199
+ fn: 'NotFound',
200
+ message: 'Children not found or Insufficient Privileges',
201
+ };
202
+ resolve(rows);
203
+ })
204
+ .catch(err => {
205
+ if (err.name === 'CastError')
206
+ reject({ fn: 'BadParameters', message: 'Bad Audit Id' });
207
+ else reject(err);
208
+ });
209
+ });
210
+ };
211
+
212
+ // Get Audit Retest
213
+ AuditSchema.statics.getRetest = (isAdmin, auditId, userId) => {
214
+ return new Promise((resolve, reject) => {
215
+ var query = Audit.findOne({ parentId: auditId });
216
+
217
+ if (!isAdmin)
218
+ query.or([
219
+ { creator: userId },
220
+ { collaborators: userId },
221
+ { reviewers: userId },
222
+ ]);
223
+ query
224
+ .exec()
225
+ .then(row => {
226
+ if (!row)
227
+ throw { fn: 'NotFound', message: 'No retest found for this audit' };
228
+ else {
229
+ resolve(row);
230
+ }
231
+ })
232
+ .catch(err => {
233
+ reject(err);
234
+ });
235
+ });
236
+ };
237
+
238
+ // Create Audit Retest
239
+ AuditSchema.statics.createRetest = (isAdmin, auditId, userId, auditType) => {
240
+ return new Promise((resolve, reject) => {
241
+ var audit = {};
242
+ audit.creator = userId;
243
+ audit.type = 'retest';
244
+ audit.parentId = auditId;
245
+ audit.auditType = auditType;
246
+ audit.findings = [];
247
+ audit.sections = [];
248
+ audit.customFields = [];
249
+
250
+ var auditTypeSections = [];
251
+ var customSections = [];
252
+ var customFields = [];
253
+ var AuditType = mongoose.model('AuditType');
254
+
255
+ var query = Audit.findById(auditId);
256
+ if (!isAdmin)
257
+ query.or([
258
+ { creator: userId },
259
+ { collaborators: userId },
260
+ { reviewers: userId },
261
+ ]);
262
+ query
263
+ .exec()
264
+ .then(async row => {
265
+ if (!row)
266
+ throw {
267
+ fn: 'NotFound',
268
+ message: 'Audit not found or Insufficient Privileges',
269
+ };
270
+ else {
271
+ var retest = await Audit.findOne({ parentId: auditId }).exec();
272
+ if (retest)
273
+ throw {
274
+ fn: 'BadParameters',
275
+ message: 'Retest already exists for this Audit',
276
+ };
277
+ audit.name = row.name;
278
+ audit.company = row.company;
279
+ audit.client = row.client;
280
+ audit.collaborators = row.collaborators;
281
+ audit.reviewers = row.reviewers;
282
+ audit.language = row.language;
283
+ audit.scope = row.scope;
284
+ audit.findings = row.findings;
285
+ // row.findings.forEach(finding => {
286
+ // var tmpFinding = {}
287
+ // tmpFinding.title = finding.title
288
+ // tmpFinding.identifier = finding.identifier
289
+ // tmpFinding.cvssv3 = finding.cvssv3
290
+ // tmpFinding.vulnType = finding.vulnType
291
+ // tmpFinding.category = finding.category
292
+ // audit.findings.push(tmpFinding)
293
+ // })
294
+ return AuditType.getByName(auditType);
295
+ }
296
+ })
297
+ .then(row => {
298
+ if (row) {
299
+ auditTypeSections = row.sections;
300
+ var auditTypeTemplate = row.templates.find(
301
+ e => e.locale === audit.language,
302
+ );
303
+ if (auditTypeTemplate) audit.template = auditTypeTemplate.template;
304
+ var Section = mongoose.model('CustomSection');
305
+ var CustomField = mongoose.model('CustomField');
306
+ var promises = [];
307
+ promises.push(Section.getAll());
308
+ promises.push(CustomField.getAll());
309
+ return Promise.all(promises);
310
+ } else throw { fn: 'NotFound', message: 'AuditType not found' };
311
+ })
312
+ .then(resolved => {
313
+ customSections = resolved[0];
314
+ customFields = resolved[1];
315
+
316
+ customSections.forEach(section => {
317
+ // Add sections with customFields (and default text) to audit
318
+ var tmpSection = {};
319
+ if (auditTypeSections.includes(section.field)) {
320
+ tmpSection.field = section.field;
321
+ tmpSection.name = section.name;
322
+ tmpSection.customFields = [];
323
+
324
+ customFields.forEach(field => {
325
+ field = field.toObject();
326
+ if (
327
+ field.display === 'section' &&
328
+ field.displaySub === tmpSection.name
329
+ ) {
330
+ var fieldText = field.text.find(
331
+ e => e.locale === audit.language,
332
+ );
333
+ if (fieldText) fieldText = fieldText.value;
334
+ else fieldText = '';
335
+
336
+ delete field.text;
337
+ tmpSection.customFields.push({
338
+ customField: field,
339
+ text: fieldText,
340
+ });
341
+ }
342
+ });
343
+ audit.sections.push(tmpSection);
344
+ }
345
+ });
346
+
347
+ customFields.forEach(field => {
348
+ // Add customFields (and default text) to audit
349
+ field = field.toObject();
350
+ if (field.display === 'general') {
351
+ var fieldText = field.text.find(e => e.locale === audit.language);
352
+ if (fieldText) fieldText = fieldText.value;
353
+ else fieldText = '';
354
+
355
+ delete field.text;
356
+ audit.customFields.push({ customField: field, text: fieldText });
357
+ }
358
+ });
359
+
360
+ return new Audit(audit).save();
361
+ })
362
+ .then(rows => {
363
+ resolve(rows);
364
+ })
365
+ .catch(err => {
366
+ console.log(err);
367
+ if (err.name === 'ValidationError')
368
+ reject({ fn: 'BadParameters', message: 'Audit validation failed' });
369
+ else reject(err);
370
+ });
371
+ });
372
+ };
373
+
374
+ // Create audit
375
+ AuditSchema.statics.create = (audit, userId) => {
376
+ return new Promise((resolve, reject) => {
377
+ audit.creator = userId;
378
+ audit.sections = [];
379
+ audit.customFields = [];
380
+
381
+ var auditTypeSections = [];
382
+ var customSections = [];
383
+ var customFields = [];
384
+ var AuditType = mongoose.model('AuditType');
385
+ AuditType.getByName(audit.auditType)
386
+ .then(row => {
387
+ if (row) {
388
+ auditTypeSections = row.sections;
389
+ var auditTypeTemplate = row.templates.find(
390
+ e => e.locale === audit.language,
391
+ );
392
+ if (auditTypeTemplate) audit.template = auditTypeTemplate.template;
393
+ var Section = mongoose.model('CustomSection');
394
+ var CustomField = mongoose.model('CustomField');
395
+ var promises = [];
396
+ promises.push(Section.getAll());
397
+ promises.push(CustomField.getAll());
398
+ return Promise.all(promises);
399
+ } else throw { fn: 'NotFound', message: 'AuditType not found' };
400
+ })
401
+ .then(resolved => {
402
+ customSections = resolved[0];
403
+ customFields = resolved[1];
404
+
405
+ customSections.forEach(section => {
406
+ // Add sections with customFields (and default text) to audit
407
+ var tmpSection = {};
408
+ if (auditTypeSections.includes(section.field)) {
409
+ tmpSection.field = section.field;
410
+ tmpSection.name = section.name;
411
+ tmpSection.customFields = [];
412
+
413
+ customFields.forEach(field => {
414
+ field = field.toObject();
415
+ if (
416
+ field.display === 'section' &&
417
+ field.displaySub === tmpSection.name
418
+ ) {
419
+ var fieldText = field.text.find(
420
+ e => e.locale === audit.language,
421
+ );
422
+ if (fieldText) fieldText = fieldText.value;
423
+ else fieldText = '';
424
+
425
+ delete field.text;
426
+ tmpSection.customFields.push({
427
+ customField: field,
428
+ text: fieldText,
429
+ });
430
+ }
431
+ });
432
+ audit.sections.push(tmpSection);
433
+ }
434
+ });
435
+
436
+ customFields.forEach(field => {
437
+ // Add customFields (and default text) to audit
438
+ field = field.toObject();
439
+ if (field.display === 'general') {
440
+ var fieldText = field.text.find(e => e.locale === audit.language);
441
+ if (fieldText) fieldText = fieldText.value;
442
+ else fieldText = '';
443
+
444
+ delete field.text;
445
+ audit.customFields.push({ customField: field, text: fieldText });
446
+ }
447
+ });
448
+
449
+ var VulnerabilityCategory = mongoose.model('VulnerabilityCategory');
450
+ return VulnerabilityCategory.getAll();
451
+ })
452
+ .then(rows => {
453
+ // Add default sort options for each vulnerability category
454
+ audit.sortFindings = [];
455
+ rows.forEach(e => {
456
+ audit.sortFindings.push({
457
+ category: e.name,
458
+ sortValue: e.sortValue,
459
+ sortOrder: e.sortOrder,
460
+ sortAuto: e.sortAuto,
461
+ });
462
+ });
463
+
464
+ return new Audit(audit).save();
465
+ })
466
+ .then(rows => {
467
+ resolve(rows);
468
+ })
469
+ .catch(err => {
470
+ console.log(err);
471
+ if (err.name === 'ValidationError')
472
+ reject({ fn: 'BadParameters', message: 'Audit validation failed' });
473
+ else reject(err);
474
+ });
475
+ });
476
+ };
477
+
478
+ // Delete audit
479
+ AuditSchema.statics.delete = (isAdmin, auditId, userId) => {
480
+ return new Promise((resolve, reject) => {
481
+ var query = Audit.findOneAndDelete({ _id: auditId });
482
+ if (!isAdmin) query.or([{ creator: userId }]);
483
+ return query
484
+ .exec()
485
+ .then(row => {
486
+ if (!row)
487
+ throw {
488
+ fn: 'NotFound',
489
+ message: 'Audit not found or Insufficient Privileges',
490
+ };
491
+
492
+ resolve(row);
493
+ })
494
+ .catch(err => {
495
+ reject(err);
496
+ });
497
+ });
498
+ };
499
+
500
+ // Get audit general information
501
+ AuditSchema.statics.getGeneral = (isAdmin, auditId, userId) => {
502
+ return new Promise((resolve, reject) => {
503
+ var query = Audit.findById(auditId);
504
+ if (!isAdmin)
505
+ query.or([
506
+ { creator: userId },
507
+ { collaborators: userId },
508
+ { reviewers: userId },
509
+ ]);
510
+ query.populate({
511
+ path: 'client',
512
+ select: 'email firstname lastname',
513
+ populate: {
514
+ path: 'company',
515
+ select: 'name',
516
+ },
517
+ });
518
+ query.populate('creator', 'username firstname lastname');
519
+ query.populate('collaborators', 'username firstname lastname');
520
+ query.populate('reviewers', 'username firstname lastname');
521
+ query.populate('company');
522
+ query.select(
523
+ 'name auditType date date_start date_end client collaborators language scope.name template customFields',
524
+ );
525
+ query
526
+ .lean()
527
+ .exec()
528
+ .then(row => {
529
+ if (!row)
530
+ throw {
531
+ fn: 'NotFound',
532
+ message: 'Audit not found or Insufficient Privileges',
533
+ };
534
+
535
+ var formatScope = row.scope.map(item => {
536
+ return item.name;
537
+ });
538
+ for (var i = 0; i < formatScope.length; i++) {
539
+ row.scope[i] = formatScope[i];
540
+ }
541
+ resolve(row);
542
+ })
543
+ .catch(err => {
544
+ reject(err);
545
+ });
546
+ });
547
+ };
548
+
549
+ // Update audit general information
550
+ AuditSchema.statics.updateGeneral = (isAdmin, auditId, userId, update) => {
551
+ return new Promise(async (resolve, reject) => {
552
+ if (update.company && update.company.name) {
553
+ var Company = mongoose.model('Company');
554
+ try {
555
+ update.company = await Company.create({ name: update.company.name });
556
+ } catch (error) {
557
+ console.log(error);
558
+ delete update.company;
559
+ }
560
+ }
561
+ var query = Audit.findByIdAndUpdate(auditId, update);
562
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
563
+ query
564
+ .exec()
565
+ .then(row => {
566
+ if (!row)
567
+ throw {
568
+ fn: 'NotFound',
569
+ message: 'Audit not found or Insufficient Privileges',
570
+ };
571
+
572
+ resolve('Audit General updated successfully');
573
+ })
574
+ .catch(err => {
575
+ reject(err);
576
+ });
577
+ });
578
+ };
579
+
580
+ // Get audit Network information
581
+ AuditSchema.statics.getNetwork = (isAdmin, auditId, userId) => {
582
+ return new Promise((resolve, reject) => {
583
+ var query = Audit.findById(auditId);
584
+ if (!isAdmin)
585
+ query.or([
586
+ { creator: userId },
587
+ { collaborators: userId },
588
+ { reviewers: userId },
589
+ ]);
590
+ query.select('scope');
591
+ query
592
+ .exec()
593
+ .then(row => {
594
+ if (!row)
595
+ throw {
596
+ fn: 'NotFound',
597
+ message: 'Audit not found or Insufficient Privileges',
598
+ };
599
+
600
+ resolve(row);
601
+ })
602
+ .catch(err => {
603
+ reject(err);
604
+ });
605
+ });
606
+ };
607
+
608
+ // Update audit Network information
609
+ AuditSchema.statics.updateNetwork = (isAdmin, auditId, userId, scope) => {
610
+ return new Promise((resolve, reject) => {
611
+ var query = Audit.findByIdAndUpdate(auditId, scope);
612
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
613
+ query
614
+ .exec()
615
+ .then(row => {
616
+ if (!row)
617
+ throw {
618
+ fn: 'NotFound',
619
+ message: 'Audit not found or Insufficient Privileges',
620
+ };
621
+
622
+ resolve('Audit Network updated successfully');
623
+ })
624
+ .catch(err => {
625
+ reject(err);
626
+ });
627
+ });
628
+ };
629
+
630
+ // Create finding
631
+ AuditSchema.statics.createFinding = (isAdmin, auditId, userId, finding) => {
632
+ return new Promise((resolve, reject) => {
633
+ Audit.getLastFindingIdentifier(auditId).then(identifier => {
634
+ finding.identifier = ++identifier;
635
+
636
+ var query = Audit.findByIdAndUpdate(auditId, {
637
+ $push: { findings: finding },
638
+ });
639
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
640
+ return query
641
+ .exec()
642
+ .then(row => {
643
+ if (!row)
644
+ throw {
645
+ fn: 'NotFound',
646
+ message: 'Audit not found or Insufficient Privileges',
647
+ };
648
+ else {
649
+ var sortOption = row.sortFindings.find(
650
+ e => e.category === (finding.category || 'No Category'),
651
+ );
652
+ if ((sortOption && sortOption.sortAuto) || !sortOption)
653
+ // if sort is set to automatic or undefined then we sort (default sort will be applied to undefined sortOption)
654
+ return Audit.updateSortFindings(isAdmin, auditId, userId, null);
655
+ // if manual sorting then we do not sort
656
+ else resolve('Audit Finding created succesfully');
657
+ }
658
+ })
659
+ .then(() => {
660
+ resolve('Audit Finding created successfully');
661
+ })
662
+ .catch(err => {
663
+ reject(err);
664
+ });
665
+ });
666
+ });
667
+ };
668
+
669
+ AuditSchema.statics.getLastFindingIdentifier = auditId => {
670
+ return new Promise((resolve, reject) => {
671
+ var query = Audit.aggregate([
672
+ { $match: { _id: new mongoose.Types.ObjectId(auditId) } },
673
+ ]);
674
+ query.unwind('findings');
675
+ query.sort({ 'findings.identifier': -1 });
676
+ query
677
+ .exec()
678
+ .then(row => {
679
+ if (!row) throw { fn: 'NotFound', message: 'Audit not found' };
680
+ else if (row.length === 0 || !row[0].findings.identifier) resolve(0);
681
+ else resolve(row[0].findings.identifier);
682
+ })
683
+ .catch(err => {
684
+ reject(err);
685
+ });
686
+ });
687
+ };
688
+
689
+ // Get finding of audit
690
+ AuditSchema.statics.getFinding = (isAdmin, auditId, userId, findingId) => {
691
+ return new Promise((resolve, reject) => {
692
+ var query = Audit.findById(auditId);
693
+ if (!isAdmin)
694
+ query.or([
695
+ { creator: userId },
696
+ { collaborators: userId },
697
+ { reviewers: userId },
698
+ ]);
699
+ query.select('findings');
700
+ query
701
+ .exec()
702
+ .then(row => {
703
+ if (!row)
704
+ throw {
705
+ fn: 'NotFound',
706
+ message: 'Audit not found or Insufficient Privileges',
707
+ };
708
+
709
+ var finding = row.findings.id(findingId);
710
+ if (finding === null)
711
+ throw { fn: 'NotFound', message: 'Finding not found' };
712
+ else resolve(finding);
713
+ })
714
+ .catch(err => {
715
+ reject(err);
716
+ });
717
+ });
718
+ };
719
+
720
+ // Update finding of audit
721
+ AuditSchema.statics.updateFinding = (
722
+ isAdmin,
723
+ auditId,
724
+ userId,
725
+ findingId,
726
+ newFinding,
727
+ ) => {
728
+ return new Promise((resolve, reject) => {
729
+ var sortAuto = true;
730
+
731
+ var query = Audit.findById(auditId);
732
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
733
+ query
734
+ .exec()
735
+ .then(row => {
736
+ if (!row)
737
+ throw {
738
+ fn: 'NotFound',
739
+ message: 'Audit not found or Insufficient Privileges',
740
+ };
741
+
742
+ var finding = row.findings.id(findingId);
743
+ if (finding === null)
744
+ reject({ fn: 'NotFound', message: 'Finding not found' });
745
+ else {
746
+ var sortOption = row.sortFindings.find(
747
+ e => e.category === (newFinding.category || 'No Category'),
748
+ );
749
+ if (sortOption && !sortOption.sortAuto) sortAuto = false;
750
+
751
+ Object.keys(newFinding).forEach(key => {
752
+ finding[key] = newFinding[key];
753
+ });
754
+ return row.save({ validateBeforeSave: false }); // Disable schema validation since scope changed from Array to String
755
+ }
756
+ })
757
+ .then(() => {
758
+ if (sortAuto)
759
+ return Audit.updateSortFindings(isAdmin, auditId, userId, null);
760
+ else resolve('Audit Finding updated successfully');
761
+ })
762
+ .then(() => {
763
+ resolve('Audit Finding updated successfully');
764
+ })
765
+ .catch(err => {
766
+ reject(err);
767
+ });
768
+ });
769
+ };
770
+
771
+ // Delete finding of audit
772
+ AuditSchema.statics.deleteFinding = (isAdmin, auditId, userId, findingId) => {
773
+ return new Promise((resolve, reject) => {
774
+ var query = Audit.findById(auditId);
775
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
776
+ query.select('findings');
777
+ query
778
+ .exec()
779
+ .then(row => {
780
+ if (!row)
781
+ throw {
782
+ fn: 'NotFound',
783
+ message: 'Audit not found or Insufficient Privileges',
784
+ };
785
+
786
+ var finding = row.findings.id(findingId);
787
+ if (finding === null)
788
+ reject({ fn: 'NotFound', message: 'Finding not found' });
789
+ else {
790
+ row.findings.pull(findingId);
791
+ return row.save();
792
+ }
793
+ })
794
+ .then(() => {
795
+ resolve('Audit Finding deleted successfully');
796
+ })
797
+ .catch(err => {
798
+ reject(err);
799
+ });
800
+ });
801
+ };
802
+
803
+ // Create section
804
+ AuditSchema.statics.createSection = (isAdmin, auditId, userId, section) => {
805
+ return new Promise((resolve, reject) => {
806
+ var query = Audit.findOneAndUpdate(
807
+ { _id: auditId, 'sections.field': { $ne: section.field } },
808
+ { $push: { sections: section } },
809
+ );
810
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
811
+ query
812
+ .exec()
813
+ .then(row => {
814
+ if (!row)
815
+ throw {
816
+ fn: 'NotFound',
817
+ message:
818
+ 'Audit not found or Section already exists or Insufficient Privileges',
819
+ };
820
+
821
+ resolve('Audit Section created successfully');
822
+ })
823
+ .catch(err => {
824
+ reject(err);
825
+ });
826
+ });
827
+ };
828
+
829
+ // Get section of audit
830
+ AuditSchema.statics.getSection = (isAdmin, auditId, userId, sectionId) => {
831
+ return new Promise((resolve, reject) => {
832
+ var query = Audit.findById(auditId);
833
+ if (!isAdmin)
834
+ query.or([
835
+ { creator: userId },
836
+ { collaborators: userId },
837
+ { reviewers: userId },
838
+ ]);
839
+
840
+ query.select('sections');
841
+ query
842
+ .exec()
843
+ .then(row => {
844
+ if (!row)
845
+ throw {
846
+ fn: 'NotFound',
847
+ message: 'Audit not found or Insufficient Privileges',
848
+ };
849
+
850
+ var section = row.sections.id(sectionId);
851
+ if (section === null)
852
+ throw { fn: 'NotFound', message: 'Section id not found' };
853
+ else resolve(section);
854
+ })
855
+ .catch(err => {
856
+ reject(err);
857
+ });
858
+ });
859
+ };
860
+
861
+ // Update section of audit
862
+ AuditSchema.statics.updateSection = (
863
+ isAdmin,
864
+ auditId,
865
+ userId,
866
+ sectionId,
867
+ newSection,
868
+ ) => {
869
+ return new Promise((resolve, reject) => {
870
+ var query = Audit.findById(auditId);
871
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
872
+ query
873
+ .exec()
874
+ .then(row => {
875
+ if (!row)
876
+ throw {
877
+ fn: 'NotFound',
878
+ message: 'Audit not found or Insufficient Privileges',
879
+ };
880
+
881
+ var section = row.sections.id(sectionId);
882
+ if (section === null)
883
+ throw { fn: 'NotFound', message: 'Section not found' };
884
+ else {
885
+ Object.keys(newSection).forEach(key => {
886
+ section[key] = newSection[key];
887
+ });
888
+ return row.save();
889
+ }
890
+ })
891
+ .then(() => {
892
+ resolve('Audit Section updated successfully');
893
+ })
894
+ .catch(err => {
895
+ reject(err);
896
+ });
897
+ });
898
+ };
899
+
900
+ // Delete section of audit
901
+ AuditSchema.statics.deleteSection = (isAdmin, auditId, userId, sectionId) => {
902
+ return new Promise((resolve, reject) => {
903
+ var query = Audit.findById(auditId);
904
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
905
+ query.select('sections');
906
+ query
907
+ .exec()
908
+ .then(row => {
909
+ if (!row)
910
+ throw {
911
+ fn: 'NotFound',
912
+ message: 'Audit not found or Insufficient Privileges',
913
+ };
914
+
915
+ var section = row.sections.id(sectionId);
916
+ if (section === null)
917
+ throw { fn: 'NotFound', message: 'Section not found' };
918
+ else {
919
+ row.sections.pull(sectionId);
920
+ return row.save();
921
+ }
922
+ })
923
+ .then(() => {
924
+ resolve('Audit Section deleted successfully');
925
+ })
926
+ .catch(err => {
927
+ reject(err);
928
+ });
929
+ });
930
+ };
931
+
932
+ // Update audit sort options for findings and run the sorting. If update param is null then just run sorting
933
+ (AuditSchema.statics.updateSortFindings = (
934
+ isAdmin,
935
+ auditId,
936
+ userId,
937
+ update,
938
+ ) => {
939
+ return new Promise((resolve, reject) => {
940
+ var audit = {};
941
+ var query = Audit.findById(auditId);
942
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
943
+ query
944
+ .exec()
945
+ .then(row => {
946
+ if (!row)
947
+ throw {
948
+ fn: 'NotFound',
949
+ message: 'Audit not found or Insufficient Privileges',
950
+ };
951
+ else {
952
+ audit = row;
953
+ if (update)
954
+ // if update is null then we only sort findings (no sort options saving)
955
+ audit.sortFindings = update.sortFindings; // saving sort options to audit
956
+
957
+ var VulnerabilityCategory = mongoose.model('VulnerabilityCategory');
958
+ return VulnerabilityCategory.getAll();
959
+ }
960
+ })
961
+ .then(row => {
962
+ var _ = require('lodash');
963
+ var findings = [];
964
+ var categoriesOrder = row.map(e => e.name);
965
+ categoriesOrder.push('undefined'); // Put uncategorized findings at the end
966
+
967
+ // Group findings by category
968
+ var findingList = _.chain(audit.findings)
969
+ .groupBy('category')
970
+ .toPairs()
971
+ .sort(
972
+ (a, b) =>
973
+ categoriesOrder.indexOf(a[0]) - categoriesOrder.indexOf(b[0]),
974
+ )
975
+ .fromPairs()
976
+ .map((value, key) => {
977
+ if (key === 'undefined') key = 'No Category';
978
+ var sortOption = audit.sortFindings.find(
979
+ option => option.category === key,
980
+ ); // Get sort option saved in audit
981
+ if (!sortOption)
982
+ // no option for category in audit
983
+ sortOption = row.find(e => e.name === key); // Get sort option from default in vulnerability category
984
+ if (!sortOption)
985
+ // no default option or category don't exist
986
+ sortOption = {
987
+ sortValue: 'cvssScore',
988
+ sortOrder: 'desc',
989
+ sortAuto: true,
990
+ }; // set a default sort option
991
+
992
+ return { category: key, findings: value, sortOption: sortOption };
993
+ })
994
+ .value();
995
+
996
+ findingList.forEach(group => {
997
+ var order = -1; // desc
998
+ if (group.sortOption.sortOrder === 'asc') order = 1;
999
+
1000
+ var tmpFindings = group.findings.sort((a, b) => {
1001
+ var cvssA = CVSS31.calculateCVSSFromVector(a.cvssv3);
1002
+ var cvssB = CVSS31.calculateCVSSFromVector(b.cvssv3);
1003
+
1004
+ // Get built-in value (findings[sortValue])
1005
+ var left = a[group.sortOption.sortValue];
1006
+
1007
+ // If sort value is a CVSS Score calculate it
1008
+ if (cvssA.success && group.sortOption.sortValue === 'cvssScore')
1009
+ left = cvssA.baseMetricScore;
1010
+ else if (
1011
+ cvssA.success &&
1012
+ group.sortOption.sortValue === 'cvssTemporalScore'
1013
+ )
1014
+ left = cvssA.temporalMetricScore;
1015
+ else if (
1016
+ cvssA.success &&
1017
+ group.sortOption.sortValue === 'cvssEnvironmentalScore'
1018
+ )
1019
+ left = cvssA.environmentalMetricScore;
1020
+
1021
+ // Not found then get customField sortValue
1022
+ if (!left) {
1023
+ left = a.customFields.find(
1024
+ e => e.customField.label === group.sortOption.sortValue,
1025
+ );
1026
+ if (left) left = left.text;
1027
+ }
1028
+ // Not found then set default to 0
1029
+ if (!left) left = 0;
1030
+ // Convert to string in case of int value
1031
+ left = left.toString();
1032
+
1033
+ // Same for right value to compare
1034
+ var right = b[group.sortOption.sortValue];
1035
+
1036
+ if (cvssB.success && group.sortOption.sortValue === 'cvssScore')
1037
+ right = cvssB.baseMetricScore;
1038
+ else if (
1039
+ cvssB.success &&
1040
+ group.sortOption.sortValue === 'cvssTemporalScore'
1041
+ )
1042
+ right = cvssB.temporalMetricScore;
1043
+ else if (
1044
+ cvssB.success &&
1045
+ group.sortOption.sortValue === 'cvssEnvironmentalScore'
1046
+ )
1047
+ right = cvssB.environmentalMetricScore;
1048
+
1049
+ if (!right) {
1050
+ right = b.customFields.find(
1051
+ e => e.customField.label === group.sortOption.sortValue,
1052
+ );
1053
+ if (right) right = right.text;
1054
+ }
1055
+ if (!right) right = 0;
1056
+ right = right.toString();
1057
+ return (
1058
+ left.localeCompare(right, undefined, { numeric: true }) * order
1059
+ );
1060
+ });
1061
+
1062
+ findings = findings.concat(tmpFindings);
1063
+ });
1064
+
1065
+ audit.findings = findings;
1066
+
1067
+ return audit.save();
1068
+ })
1069
+ .then(() => {
1070
+ resolve('Audit findings sorted successfully');
1071
+ })
1072
+ .catch(err => {
1073
+ console.log(err);
1074
+ reject(err);
1075
+ });
1076
+ });
1077
+ }),
1078
+ // Move finding from move.oldIndex to move.newIndex
1079
+ (AuditSchema.statics.moveFindingPosition = (
1080
+ isAdmin,
1081
+ auditId,
1082
+ userId,
1083
+ move,
1084
+ ) => {
1085
+ return new Promise((resolve, reject) => {
1086
+ var query = Audit.findById(auditId);
1087
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
1088
+ query
1089
+ .exec()
1090
+ .then(row => {
1091
+ if (!row)
1092
+ throw {
1093
+ fn: 'NotFound',
1094
+ message: 'Audit not found or Insufficient Privileges',
1095
+ };
1096
+
1097
+ var tmp = row.findings[move.oldIndex];
1098
+ row.findings.splice(move.oldIndex, 1);
1099
+ row.findings.splice(move.newIndex, 0, tmp);
1100
+
1101
+ row.markModified('findings');
1102
+ return row.save();
1103
+ })
1104
+ .then(msg => {
1105
+ resolve('Audit Finding moved successfully');
1106
+ })
1107
+ .catch(err => {
1108
+ reject(err);
1109
+ });
1110
+ });
1111
+ });
1112
+
1113
+ AuditSchema.statics.updateApprovals = (isAdmin, auditId, userId, update) => {
1114
+ return new Promise(async (resolve, reject) => {
1115
+ var Settings = mongoose.model('Settings');
1116
+ var settings = await Settings.getAll();
1117
+
1118
+ if (update.approvals.length >= settings.reviews.public.minReviewers) {
1119
+ update.state = 'APPROVED';
1120
+ } else {
1121
+ update.state = 'REVIEW';
1122
+ }
1123
+
1124
+ var query = Audit.findByIdAndUpdate(auditId, update);
1125
+ query.nor([{ creator: userId }, { collaborators: userId }]);
1126
+ if (!isAdmin) query.or([{ reviewers: userId }]);
1127
+
1128
+ query
1129
+ .exec()
1130
+ .then(row => {
1131
+ if (!row)
1132
+ throw {
1133
+ fn: 'NotFound',
1134
+ message: 'Audit not found or Insufficient Privileges',
1135
+ };
1136
+
1137
+ resolve('Audit approvals updated successfully');
1138
+ })
1139
+ .catch(err => {
1140
+ reject(err);
1141
+ });
1142
+ });
1143
+ };
1144
+
1145
+ // Update audit parent
1146
+ AuditSchema.statics.updateParent = (isAdmin, auditId, userId, parentId) => {
1147
+ return new Promise(async (resolve, reject) => {
1148
+ var query = Audit.findByIdAndUpdate(auditId, { parentId: parentId });
1149
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
1150
+ query
1151
+ .exec()
1152
+ .then(row => {
1153
+ if (!row)
1154
+ throw {
1155
+ fn: 'NotFound',
1156
+ message: 'Audit not found or Insufficient Privileges',
1157
+ };
1158
+
1159
+ resolve('Audit Parent updated successfully');
1160
+ })
1161
+ .catch(err => {
1162
+ reject(err);
1163
+ });
1164
+ });
1165
+ };
1166
+
1167
+ // Delete audit parent
1168
+ AuditSchema.statics.deleteParent = (isAdmin, auditId, userId) => {
1169
+ return new Promise(async (resolve, reject) => {
1170
+ var query = Audit.findByIdAndUpdate(auditId, { parentId: null });
1171
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
1172
+ query
1173
+ .exec()
1174
+ .then(row => {
1175
+ if (!row)
1176
+ throw {
1177
+ fn: 'NotFound',
1178
+ message: 'Audit not found or Insufficient Privileges',
1179
+ };
1180
+
1181
+ resolve(row);
1182
+ })
1183
+ .catch(err => {
1184
+ reject(err);
1185
+ });
1186
+ });
1187
+ };
1188
+
1189
+ /*
1190
+ *** Methods ***
1191
+ */
1192
+
1193
+ var Audit = mongoose.model('Audit', AuditSchema);
1194
+ // Audit.syncIndexes()
1195
+ module.exports = Audit;
backend/src/models/client.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var ClientSchema = new Schema(
5
+ {
6
+ email: { type: String, required: true, unique: true },
7
+ company: { type: Schema.Types.ObjectId, ref: 'Company' },
8
+ lastname: String,
9
+ firstname: String,
10
+ phone: String,
11
+ cell: String,
12
+ title: String,
13
+ },
14
+ { timestamps: true },
15
+ );
16
+
17
+ /*
18
+ *** Statics ***
19
+ */
20
+
21
+ // Get all clients
22
+ ClientSchema.statics.getAll = () => {
23
+ return new Promise((resolve, reject) => {
24
+ var query = Client.find().populate('company', '-_id name');
25
+ query.select('email lastname firstname phone cell title');
26
+ query
27
+ .exec()
28
+ .then(rows => {
29
+ resolve(rows);
30
+ })
31
+ .catch(err => {
32
+ reject(err);
33
+ });
34
+ });
35
+ };
36
+
37
+ // Create client
38
+ ClientSchema.statics.create = (client, company) => {
39
+ return new Promise(async (resolve, reject) => {
40
+ if (company) {
41
+ var Company = mongoose.model('Company');
42
+ var query = Company.findOneAndUpdate(
43
+ { name: company },
44
+ {},
45
+ { upsert: true, new: true },
46
+ );
47
+ var companyRow = await query.exec();
48
+ if (companyRow) client.company = companyRow._id;
49
+ }
50
+ var query = new Client(client);
51
+ query
52
+ .save(company)
53
+ .then(row => {
54
+ resolve({
55
+ _id: row._id,
56
+ email: row.email,
57
+ firstname: row.firstname,
58
+ lastname: row.lastname,
59
+ title: row.title,
60
+ phone: row.phone,
61
+ cell: row.cell,
62
+ company: row.company,
63
+ });
64
+ })
65
+ .catch(err => {
66
+ if (err.code === 11000)
67
+ reject({
68
+ fn: 'BadParameters',
69
+ message: 'Client email already exists',
70
+ });
71
+ else reject(err);
72
+ });
73
+ });
74
+ };
75
+
76
+ // Update client
77
+ ClientSchema.statics.update = (clientId, client, company) => {
78
+ return new Promise(async (resolve, reject) => {
79
+ if (company) {
80
+ var Company = mongoose.model('Company');
81
+ var query = Company.findOneAndUpdate(
82
+ { name: company },
83
+ {},
84
+ { upsert: true, new: true },
85
+ );
86
+ var companyRow = await query.exec();
87
+ if (companyRow) client.company = companyRow.id;
88
+ }
89
+ var query = Client.findOneAndUpdate({ _id: clientId }, client);
90
+ query
91
+ .exec()
92
+ .then(rows => {
93
+ if (rows) resolve(rows);
94
+ else reject({ fn: 'NotFound', message: 'Client Id not found' });
95
+ })
96
+ .catch(err => {
97
+ if (err.code === 11000)
98
+ reject({
99
+ fn: 'BadParameters',
100
+ message: 'Client email already exists',
101
+ });
102
+ else reject(err);
103
+ });
104
+ });
105
+ };
106
+
107
+ // Delete client
108
+ ClientSchema.statics.delete = clientId => {
109
+ return new Promise((resolve, reject) => {
110
+ var query = Client.findOneAndDelete({ _id: clientId });
111
+ query
112
+ .exec()
113
+ .then(rows => {
114
+ if (rows) resolve(rows);
115
+ else reject({ fn: 'NotFound', message: 'Client Id not found' });
116
+ })
117
+ .catch(err => {
118
+ reject(err);
119
+ });
120
+ });
121
+ };
122
+
123
+ /*
124
+ *** Methods ***
125
+ */
126
+
127
+ var Client = mongoose.model('Client', ClientSchema);
128
+ module.exports = Client;
backend/src/models/company.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var CompanySchema = new Schema(
5
+ {
6
+ name: { type: String, required: true, unique: true },
7
+ shortName: String,
8
+ logo: String,
9
+ },
10
+ { timestamps: true },
11
+ );
12
+
13
+ /*
14
+ *** Statics ***
15
+ */
16
+
17
+ // Get all companies
18
+ CompanySchema.statics.getAll = () => {
19
+ return new Promise((resolve, reject) => {
20
+ var query = Company.find();
21
+ query.select('name shortName logo');
22
+ query
23
+ .exec()
24
+ .then(rows => {
25
+ resolve(rows);
26
+ })
27
+ .catch(err => {
28
+ reject(err);
29
+ });
30
+ });
31
+ };
32
+
33
+ // Create company
34
+ CompanySchema.statics.create = company => {
35
+ return new Promise((resolve, reject) => {
36
+ var query = new Company(company);
37
+ query
38
+ .save(company)
39
+ .then(row => {
40
+ resolve({ _id: row._id, name: row.name });
41
+ })
42
+ .catch(err => {
43
+ if (err.code === 11000)
44
+ reject({
45
+ fn: 'BadParameters',
46
+ message: 'Company name already exists',
47
+ });
48
+ else reject(err);
49
+ });
50
+ });
51
+ };
52
+
53
+ // Update company
54
+ CompanySchema.statics.update = (companyId, company) => {
55
+ return new Promise((resolve, reject) => {
56
+ var query = Company.findOneAndUpdate({ _id: companyId }, company);
57
+ query
58
+ .exec()
59
+ .then(rows => {
60
+ if (rows) resolve(rows);
61
+ else reject({ fn: 'NotFound', message: 'Company Id not found' });
62
+ })
63
+ .catch(err => {
64
+ if (err.code === 11000)
65
+ reject({
66
+ fn: 'BadParameters',
67
+ message: 'Company name already exists',
68
+ });
69
+ else reject(err);
70
+ });
71
+ });
72
+ };
73
+
74
+ // Delete company
75
+ CompanySchema.statics.delete = companyId => {
76
+ return new Promise((resolve, reject) => {
77
+ var query = Company.findOneAndDelete({ _id: companyId });
78
+ query
79
+ .exec()
80
+ .then(rows => {
81
+ if (rows) resolve(rows);
82
+ else reject({ fn: 'NotFound', message: 'Company Id not found' });
83
+ })
84
+ .catch(err => {
85
+ reject(err);
86
+ });
87
+ });
88
+ };
89
+
90
+ /*
91
+ *** Methods ***
92
+ */
93
+
94
+ var Company = mongoose.model('Company', CompanySchema);
95
+ module.exports = Company;
backend/src/models/custom-field.js ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose'); //.set('debug', true);
2
+ var Schema = mongoose.Schema;
3
+
4
+ var CustomFieldSchema = new Schema(
5
+ {
6
+ fieldType: String,
7
+ label: String,
8
+ display: String,
9
+ displaySub: { type: String, default: '' },
10
+ position: Number,
11
+ size: {
12
+ type: Number,
13
+ enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
14
+ default: 12,
15
+ },
16
+ offset: {
17
+ type: Number,
18
+ enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
19
+ default: 0,
20
+ },
21
+ required: { type: Boolean, default: false },
22
+ description: { type: String, default: '' },
23
+ text: [{ _id: false, locale: String, value: Schema.Types.Mixed }],
24
+ options: [{ _id: false, locale: String, value: String }],
25
+ },
26
+ { timestamps: true },
27
+ );
28
+
29
+ CustomFieldSchema.index(
30
+ { label: 1, display: 1, displaySub: 1 },
31
+ {
32
+ name: 'unique_label_display',
33
+ unique: true,
34
+ partialFilterExpression: { label: { $exists: true, $gt: '' } },
35
+ },
36
+ );
37
+
38
+ /*
39
+ *** Statics ***
40
+ */
41
+
42
+ // Get all Fields
43
+ CustomFieldSchema.statics.getAll = () => {
44
+ return new Promise((resolve, reject) => {
45
+ var query = CustomField.find().sort('position');
46
+ query.select(
47
+ 'fieldType label display displaySub size offset required description text options',
48
+ );
49
+ query
50
+ .exec()
51
+ .then(rows => {
52
+ resolve(rows);
53
+ })
54
+ .catch(err => {
55
+ reject(err);
56
+ });
57
+ });
58
+ };
59
+
60
+ // Create Field
61
+ CustomFieldSchema.statics.create = field => {
62
+ return new Promise((resolve, reject) => {
63
+ var query = new CustomField(field);
64
+ query
65
+ .save()
66
+ .then(row => {
67
+ resolve(row);
68
+ })
69
+ .catch(err => {
70
+ if (err.code === 11000)
71
+ reject({
72
+ fn: 'BadParameters',
73
+ message: 'Custom Field already exists',
74
+ });
75
+ else reject(err);
76
+ });
77
+ });
78
+ };
79
+
80
+ // Update Fields
81
+ CustomFieldSchema.statics.updateAll = fields => {
82
+ return new Promise((resolve, reject) => {
83
+ var promises = fields.map(field => {
84
+ return CustomField.findByIdAndUpdate(field._id, field).exec();
85
+ });
86
+ return Promise.all(promises)
87
+ .then(row => {
88
+ resolve('Fields updated successfully');
89
+ })
90
+ .catch(err => {
91
+ reject(err);
92
+ });
93
+ });
94
+ };
95
+
96
+ // Delete Field
97
+ CustomFieldSchema.statics.delete = fieldId => {
98
+ return new Promise((resolve, reject) => {
99
+ var pullCount = 0;
100
+ var Vulnerability = mongoose.model('Vulnerability');
101
+ var query = Vulnerability.find();
102
+ query
103
+ .exec()
104
+ .then(rows => {
105
+ var promises = [];
106
+ promises.push(CustomField.findByIdAndDelete(fieldId).exec());
107
+ rows.map(row => {
108
+ row.details.map(detail => {
109
+ if (
110
+ detail.customFields.some(
111
+ field => `${field.customField}` === fieldId,
112
+ )
113
+ )
114
+ pullCount++;
115
+
116
+ detail.customFields.pull({ customField: fieldId });
117
+ });
118
+ promises.push(row.save());
119
+ });
120
+ return Promise.all(promises);
121
+ })
122
+ .then(row => {
123
+ if (row && row[0])
124
+ resolve({
125
+ msg: `Custom Field deleted successfully`,
126
+ vulnCount: pullCount,
127
+ });
128
+ else
129
+ reject({
130
+ fn: 'NotFound',
131
+ message: { msg: 'Custom Field not found', vulnCount: pullCount },
132
+ });
133
+ })
134
+ .catch(err => {
135
+ console.log(err);
136
+ reject(err);
137
+ });
138
+ });
139
+ };
140
+
141
+ /*
142
+ *** Methods ***
143
+ */
144
+
145
+ var CustomField = mongoose.model('CustomField', CustomFieldSchema);
146
+ CustomField.syncIndexes();
147
+ module.exports = CustomField;
backend/src/models/custom-section.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var CustomSectionSchema = new Schema(
5
+ {
6
+ field: { type: String, required: true, unique: true },
7
+ name: { type: String, required: true, unique: true },
8
+ icon: String,
9
+ },
10
+ { timestamps: true },
11
+ );
12
+
13
+ /*
14
+ *** Statics ***
15
+ */
16
+
17
+ // Get all Sections
18
+ CustomSectionSchema.statics.getAll = () => {
19
+ return new Promise((resolve, reject) => {
20
+ var query = CustomSection.find();
21
+ query.select('-_id field name icon');
22
+ query
23
+ .exec()
24
+ .then(rows => {
25
+ resolve(rows);
26
+ })
27
+ .catch(err => {
28
+ reject(err);
29
+ });
30
+ });
31
+ };
32
+
33
+ // Get all Sections by Language
34
+ CustomSectionSchema.statics.getAllByLanguage = locale => {
35
+ return new Promise((resolve, reject) => {
36
+ var query = CustomSection.find({ locale: locale });
37
+ query.select('-_id field name icon');
38
+ query
39
+ .exec()
40
+ .then(rows => {
41
+ resolve(rows);
42
+ })
43
+ .catch(err => {
44
+ reject(err);
45
+ });
46
+ });
47
+ };
48
+
49
+ // Create Section
50
+ CustomSectionSchema.statics.create = section => {
51
+ return new Promise((resolve, reject) => {
52
+ var query = new CustomSection(section);
53
+ query
54
+ .save()
55
+ .then(row => {
56
+ resolve(row);
57
+ })
58
+ .catch(err => {
59
+ if (err.code === 11000)
60
+ reject({
61
+ fn: 'BadParameters',
62
+ message: 'Custom Section already exists',
63
+ });
64
+ else reject(err);
65
+ });
66
+ });
67
+ };
68
+
69
+ // Update Sections
70
+ CustomSectionSchema.statics.updateAll = sections => {
71
+ return new Promise((resolve, reject) => {
72
+ CustomSection.deleteMany()
73
+ .then(row => {
74
+ CustomSection.insertMany(sections);
75
+ })
76
+ .then(row => {
77
+ resolve('Sections updated successfully');
78
+ })
79
+ .catch(err => {
80
+ reject(err);
81
+ });
82
+ });
83
+ };
84
+
85
+ // Delete Section
86
+ CustomSectionSchema.statics.delete = (field, locale) => {
87
+ return new Promise((resolve, reject) => {
88
+ CustomSection.deleteOne({ field: field, locale: locale })
89
+ .then(res => {
90
+ if (res.deletedCount === 1) resolve('Custom Section deleted');
91
+ else reject({ fn: 'NotFound', message: 'Custom Section not found' });
92
+ })
93
+ .catch(err => {
94
+ reject(err);
95
+ });
96
+ });
97
+ };
98
+
99
+ /*
100
+ *** Methods ***
101
+ */
102
+
103
+ var CustomSection = mongoose.model('CustomSection', CustomSectionSchema);
104
+ CustomSection.syncIndexes();
105
+ module.exports = CustomSection;
backend/src/models/image.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var ImageSchema = new Schema(
5
+ {
6
+ auditId: { type: Schema.Types.ObjectId, ref: 'Audit' },
7
+ value: { type: String, required: true, unique: true },
8
+ name: String,
9
+ },
10
+ { timestamps: true },
11
+ );
12
+
13
+ /*
14
+ *** Statics ***
15
+ */
16
+
17
+ // Get one image
18
+ ImageSchema.statics.getOne = imageId => {
19
+ return new Promise((resolve, reject) => {
20
+ var query = Image.findById(imageId);
21
+
22
+ query.select('auditId value name');
23
+ query
24
+ .exec()
25
+ .then(row => {
26
+ if (row) resolve(row);
27
+ else throw { fn: 'NotFound', message: 'Image not found' };
28
+ })
29
+ .catch(err => {
30
+ reject(err);
31
+ });
32
+ });
33
+ };
34
+
35
+ // Create image
36
+ ImageSchema.statics.create = image => {
37
+ return new Promise((resolve, reject) => {
38
+ var query = Image.findOne({ value: image.value });
39
+ query
40
+ .exec()
41
+ .then(row => {
42
+ if (row) return row;
43
+ query = new Image(image);
44
+ return query.save();
45
+ })
46
+ .then(row => {
47
+ resolve({ _id: row._id });
48
+ })
49
+ .catch(err => {
50
+ console.log(err);
51
+ reject(err);
52
+ });
53
+ });
54
+ };
55
+
56
+ // Delete image
57
+ ImageSchema.statics.delete = imageId => {
58
+ return new Promise((resolve, reject) => {
59
+ var query = Image.findByIdAndDelete(imageId);
60
+ query
61
+ .exec()
62
+ .then(rows => {
63
+ if (rows) resolve(rows);
64
+ else reject({ fn: 'NotFound', message: 'Image not found' });
65
+ })
66
+ .catch(err => {
67
+ reject(err);
68
+ });
69
+ });
70
+ };
71
+
72
+ /*
73
+ *** Methods ***
74
+ */
75
+
76
+ var Image = mongoose.model('Image', ImageSchema);
77
+ Image.syncIndexes();
78
+ module.exports = Image;
backend/src/models/language.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var LanguageSchema = new Schema(
5
+ {
6
+ language: { type: String, unique: true },
7
+ locale: { type: String, unique: true },
8
+ },
9
+ { timestamps: true },
10
+ );
11
+
12
+ /*
13
+ *** Statics ***
14
+ */
15
+
16
+ // Get all languages
17
+ LanguageSchema.statics.getAll = () => {
18
+ return new Promise((resolve, reject) => {
19
+ var query = Language.find();
20
+ query.select('-_id language locale');
21
+ query
22
+ .exec()
23
+ .then(rows => {
24
+ resolve(rows);
25
+ })
26
+ .catch(err => {
27
+ reject(err);
28
+ });
29
+ });
30
+ };
31
+
32
+ // Create language
33
+ LanguageSchema.statics.create = language => {
34
+ return new Promise((resolve, reject) => {
35
+ var query = new Language(language);
36
+ query
37
+ .save()
38
+ .then(row => {
39
+ resolve(row);
40
+ })
41
+ .catch(err => {
42
+ if (err.code === 11000)
43
+ reject({ fn: 'BadParameters', message: 'Language already exists' });
44
+ else reject(err);
45
+ });
46
+ });
47
+ };
48
+
49
+ // Update languages
50
+ LanguageSchema.statics.updateAll = languages => {
51
+ return new Promise((resolve, reject) => {
52
+ Language.deleteMany()
53
+ .then(row => {
54
+ Language.insertMany(languages);
55
+ })
56
+ .then(row => {
57
+ resolve('Languages updated successfully');
58
+ })
59
+ .catch(err => {
60
+ reject(err);
61
+ });
62
+ });
63
+ };
64
+
65
+ // Delete language
66
+ LanguageSchema.statics.delete = locale => {
67
+ return new Promise((resolve, reject) => {
68
+ Language.deleteOne({ locale: locale })
69
+ .then(res => {
70
+ if (res.deletedCount === 1) resolve('Language deleted');
71
+ else reject({ fn: 'NotFound', message: 'Language not found' });
72
+ })
73
+ .catch(err => {
74
+ reject(err);
75
+ });
76
+ });
77
+ };
78
+
79
+ /*
80
+ *** Methods ***
81
+ */
82
+
83
+ var Language = mongoose.model('Language', LanguageSchema);
84
+ module.exports = Language;
backend/src/models/settings.js ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose'); //.set('debug', true);
2
+ var Schema = mongoose.Schema;
3
+ var _ = require('lodash');
4
+ var Utils = require('../lib/utils.js');
5
+
6
+ // https://stackoverflow.com/questions/25822289/what-is-the-best-way-to-store-color-hex-values-in-mongodb-mongoose
7
+ const colorValidator = v => /^#([0-9a-f]{3}){1,2}$/i.test(v);
8
+
9
+ const SettingSchema = new Schema(
10
+ {
11
+ report: {
12
+ enabled: { type: Boolean, default: true },
13
+ public: {
14
+ cvssColors: {
15
+ noneColor: {
16
+ type: String,
17
+ default: '#4a86e8',
18
+ validate: [colorValidator, 'Invalid color'],
19
+ },
20
+ lowColor: {
21
+ type: String,
22
+ default: '#008000',
23
+ validate: [colorValidator, 'Invalid color'],
24
+ },
25
+ mediumColor: {
26
+ type: String,
27
+ default: '#f9a009',
28
+ validate: [colorValidator, 'Invalid color'],
29
+ },
30
+ highColor: {
31
+ type: String,
32
+ default: '#fe0000',
33
+ validate: [colorValidator, 'Invalid color'],
34
+ },
35
+ criticalColor: {
36
+ type: String,
37
+ default: '#212121',
38
+ validate: [colorValidator, 'Invalid color'],
39
+ },
40
+ },
41
+ captions: {
42
+ type: [{ type: String, unique: true }],
43
+ default: ['Figure'],
44
+ },
45
+ highlightWarning: { type: Boolean, default: false },
46
+ highlightWarningColor: {
47
+ type: String,
48
+ default: '#ffff25',
49
+ validate: [colorValidator, 'Invalid color'],
50
+ },
51
+ requiredFields: {
52
+ company: { type: Boolean, default: false },
53
+ client: { type: Boolean, default: false },
54
+ dateStart: { type: Boolean, default: false },
55
+ dateEnd: { type: Boolean, default: false },
56
+ dateReport: { type: Boolean, default: false },
57
+ scope: { type: Boolean, default: false },
58
+ findingType: { type: Boolean, default: false },
59
+ findingDescription: { type: Boolean, default: false },
60
+ findingObservation: { type: Boolean, default: false },
61
+ findingReferences: { type: Boolean, default: false },
62
+ findingProofs: { type: Boolean, default: false },
63
+ findingAffected: { type: Boolean, default: false },
64
+ findingRemediationDifficulty: { type: Boolean, default: false },
65
+ findingPriority: { type: Boolean, default: false },
66
+ findingRemediation: { type: Boolean, default: false },
67
+ },
68
+ },
69
+ private: {
70
+ imageBorder: { type: Boolean, default: false },
71
+ imageBorderColor: {
72
+ type: String,
73
+ default: '#000000',
74
+ validate: [colorValidator, 'Invalid color'],
75
+ },
76
+ },
77
+ },
78
+ reviews: {
79
+ enabled: { type: Boolean, default: false },
80
+ public: {
81
+ mandatoryReview: { type: Boolean, default: false },
82
+ minReviewers: {
83
+ type: Number,
84
+ default: 1,
85
+ min: 1,
86
+ max: 100,
87
+ validate: [Number.isInteger, 'Invalid integer'],
88
+ },
89
+ },
90
+ private: {
91
+ removeApprovalsUponUpdate: { type: Boolean, default: false },
92
+ },
93
+ },
94
+ },
95
+ { strict: true },
96
+ );
97
+
98
+ // Get all settings
99
+ SettingSchema.statics.getAll = () => {
100
+ return new Promise((resolve, reject) => {
101
+ const query = Settings.findOne({});
102
+ query.select('-_id -__v');
103
+ query
104
+ .exec()
105
+ .then(settings => {
106
+ resolve(settings);
107
+ })
108
+ .catch(err => reject(err));
109
+ });
110
+ };
111
+
112
+ // Get public settings
113
+ SettingSchema.statics.getPublic = () => {
114
+ return new Promise((resolve, reject) => {
115
+ const query = Settings.findOne({});
116
+ query.select(
117
+ '-_id report.enabled report.public reviews.enabled reviews.public',
118
+ );
119
+ query
120
+ .exec()
121
+ .then(settings => resolve(settings))
122
+ .catch(err => reject(err));
123
+ });
124
+ };
125
+
126
+ // Update Settings
127
+ SettingSchema.statics.update = settings => {
128
+ return new Promise((resolve, reject) => {
129
+ const query = Settings.findOneAndUpdate({}, settings, {
130
+ new: true,
131
+ runValidators: true,
132
+ });
133
+ query
134
+ .exec()
135
+ .then(settings => resolve(settings))
136
+ .catch(err => reject(err));
137
+ });
138
+ };
139
+
140
+ // Restore settings to default
141
+ SettingSchema.statics.restoreDefaults = () => {
142
+ return new Promise((resolve, reject) => {
143
+ const query = Settings.deleteMany({});
144
+ query
145
+ .exec()
146
+ .then(_ => {
147
+ const query = new Settings({});
148
+ query
149
+ .save()
150
+ .then(_ => resolve('Restored default settings.'))
151
+ .catch(err => reject(err));
152
+ })
153
+ .catch(err => reject(err));
154
+ });
155
+ };
156
+
157
+ const Settings = mongoose.model('Settings', SettingSchema);
158
+
159
+ // Populate/update settings when server starts
160
+ Settings.findOne()
161
+ .then(liveSettings => {
162
+ if (!liveSettings) {
163
+ console.log('Initializing Settings');
164
+ Settings.create({}).catch(err => {
165
+ throw 'Error creating the settings in the database : ' + err;
166
+ });
167
+ } else {
168
+ var needUpdate = false;
169
+ var liveSettingsPaths = Utils.getObjectPaths(liveSettings.toObject());
170
+
171
+ liveSettingsPaths.forEach(path => {
172
+ if (!SettingSchema.path(path) && !path.startsWith('_')) {
173
+ needUpdate = true;
174
+ _.set(liveSettings, path, undefined);
175
+ }
176
+ });
177
+
178
+ if (needUpdate) {
179
+ console.log('Removing unused fields from Settings');
180
+ liveSettings.save();
181
+ }
182
+ }
183
+ })
184
+ .catch(err => {
185
+ throw 'Error checking for initial settings in the database : ' + err;
186
+ });
187
+
188
+ module.exports = Settings;
backend/src/models/template.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var TemplateSchema = new Schema(
5
+ {
6
+ name: { type: String, required: true, unique: true },
7
+ ext: { type: String, required: true, unique: false },
8
+ },
9
+ { timestamps: true },
10
+ );
11
+
12
+ /*
13
+ *** Statics ***
14
+ */
15
+
16
+ // Get all templates
17
+ TemplateSchema.statics.getAll = () => {
18
+ return new Promise((resolve, reject) => {
19
+ var query = Template.find();
20
+ query.select('name ext');
21
+ query
22
+ .exec()
23
+ .then(rows => {
24
+ resolve(rows);
25
+ })
26
+ .catch(err => {
27
+ reject(err);
28
+ });
29
+ });
30
+ };
31
+
32
+ // Get one template
33
+ TemplateSchema.statics.getOne = templateId => {
34
+ return new Promise((resolve, reject) => {
35
+ var query = Template.findById(templateId);
36
+ query.select('name ext');
37
+ query
38
+ .exec()
39
+ .then(rows => {
40
+ resolve(rows);
41
+ })
42
+ .catch(err => {
43
+ reject(err);
44
+ });
45
+ });
46
+ };
47
+
48
+ // Create template
49
+ TemplateSchema.statics.create = template => {
50
+ return new Promise((resolve, reject) => {
51
+ var query = new Template(template);
52
+ query
53
+ .save()
54
+ .then(row => {
55
+ resolve({ _id: row._id, name: row.name, ext: row.ext });
56
+ })
57
+ .catch(err => {
58
+ if (err.code === 11000)
59
+ reject({
60
+ fn: 'BadParameters',
61
+ message: 'Template name already exists',
62
+ });
63
+ else reject(err);
64
+ });
65
+ });
66
+ };
67
+
68
+ // Update template
69
+ TemplateSchema.statics.update = (templateId, template) => {
70
+ return new Promise((resolve, reject) => {
71
+ var query = Template.findByIdAndUpdate(templateId, template);
72
+ query
73
+ .exec()
74
+ .then(rows => {
75
+ if (rows) resolve(rows);
76
+ else reject({ fn: 'NotFound', message: 'Template not found' });
77
+ })
78
+ .catch(err => {
79
+ if (err.code === 11000)
80
+ reject({
81
+ fn: 'BadParameters',
82
+ message: 'Template name already exists',
83
+ });
84
+ else reject(err);
85
+ });
86
+ });
87
+ };
88
+
89
+ // Delete template
90
+ TemplateSchema.statics.delete = templateId => {
91
+ return new Promise((resolve, reject) => {
92
+ var query = Template.findByIdAndDelete(templateId);
93
+ query
94
+ .exec()
95
+ .then(rows => {
96
+ if (rows) resolve(rows);
97
+ else reject({ fn: 'NotFound', message: 'Template not found' });
98
+ })
99
+ .catch(err => {
100
+ reject(err);
101
+ });
102
+ });
103
+ };
104
+
105
+ /*
106
+ *** Methods ***
107
+ */
108
+
109
+ var Template = mongoose.model('Template', TemplateSchema);
110
+ module.exports = Template;
backend/src/models/user.js ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+ var bcrypt = require('bcrypt');
4
+ var jwt = require('jsonwebtoken');
5
+
6
+ var auth = require('../lib/auth.js');
7
+ const { generateUUID } = require('../lib/utils.js');
8
+ var _ = require('lodash');
9
+
10
+ var QRCode = require('qrcode');
11
+ var OTPAuth = require('otpauth');
12
+
13
+ var UserSchema = new Schema(
14
+ {
15
+ username: { type: String, unique: true, required: true },
16
+ password: { type: String, required: true },
17
+ firstname: { type: String, required: true },
18
+ lastname: { type: String, required: true },
19
+ email: { type: String, required: false },
20
+ phone: { type: String, required: false },
21
+ role: { type: String, default: 'user' },
22
+ totpEnabled: { type: Boolean, default: false },
23
+ totpSecret: { type: String, default: '' },
24
+ enabled: { type: Boolean, default: true },
25
+ refreshTokens: [
26
+ { _id: false, sessionId: String, userAgent: String, token: String },
27
+ ],
28
+ },
29
+ { timestamps: true },
30
+ );
31
+
32
+ var totpConfig = {
33
+ issuer: 'AuditForge',
34
+ label: '',
35
+ algorithm: 'SHA1',
36
+ digits: 6,
37
+ period: 30,
38
+ secret: '',
39
+ };
40
+
41
+ //check TOTP token
42
+ var checkTotpToken = function (token, secret) {
43
+ if (!token) throw { fn: 'BadParameters', message: 'TOTP token required' };
44
+ if (token.length !== 6)
45
+ throw { fn: 'BadParameters', message: 'Invalid TOTP token length' };
46
+ if (!secret) throw { fn: 'BadParameters', message: 'TOTP secret required' };
47
+
48
+ let newConfig = totpConfig;
49
+ newConfig.secret = secret;
50
+ let totp = new OTPAuth.TOTP(newConfig);
51
+ let delta = totp.validate({
52
+ token: token,
53
+ window: 5,
54
+ });
55
+ //The token is valid in 2 windows in the past and the future, current window is 0.
56
+ if (delta === null) {
57
+ throw { fn: 'Unauthorized', message: 'Wrong TOTP token.' };
58
+ } else if (delta < -2 || delta > 2) {
59
+ throw { fn: 'Unauthorized', message: 'TOTP token out of window.' };
60
+ }
61
+ return true;
62
+ };
63
+
64
+ /*
65
+ *** Statics ***
66
+ */
67
+
68
+ // Create user
69
+ UserSchema.statics.create = function (user) {
70
+ return new Promise((resolve, reject) => {
71
+ var hash = bcrypt.hashSync(user.password, 10);
72
+ user.password = hash;
73
+ new User(user)
74
+ .save()
75
+ .then(function () {
76
+ resolve();
77
+ })
78
+ .catch(function (err) {
79
+ if (err.code === 11000)
80
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
81
+ else reject(err);
82
+ });
83
+ });
84
+ };
85
+
86
+ // Get all users
87
+ UserSchema.statics.getAll = function () {
88
+ return new Promise((resolve, reject) => {
89
+ var query = this.find();
90
+ query.select(
91
+ 'username firstname lastname email phone role totpEnabled enabled',
92
+ );
93
+ query
94
+ .exec()
95
+ .then(function (rows) {
96
+ resolve(rows);
97
+ })
98
+ .catch(function (err) {
99
+ reject(err);
100
+ });
101
+ });
102
+ };
103
+
104
+ // Get one user by its username
105
+ UserSchema.statics.getByUsername = function (username) {
106
+ return new Promise((resolve, reject) => {
107
+ var query = this.findOne({ username: username });
108
+ query.select(
109
+ 'username firstname lastname email phone role totpEnabled enabled',
110
+ );
111
+ query
112
+ .exec()
113
+ .then(function (row) {
114
+ if (row) resolve(row);
115
+ else throw { fn: 'NotFound', message: 'User not found' };
116
+ })
117
+ .catch(function (err) {
118
+ reject(err);
119
+ });
120
+ });
121
+ };
122
+
123
+ // Update user with password verification (for updating my profile)
124
+ UserSchema.statics.updateProfile = function (username, user) {
125
+ return new Promise((resolve, reject) => {
126
+ var query = this.findOne({ username: username });
127
+ var payload = {};
128
+ query
129
+ .exec()
130
+ .then(function (row) {
131
+ if (!row) throw { fn: 'NotFound', message: 'User not found' };
132
+ else if (bcrypt.compareSync(user.password, row.password)) {
133
+ if (user.username) row.username = user.username;
134
+ if (user.firstname) row.firstname = user.firstname;
135
+ if (user.lastname) row.lastname = user.lastname;
136
+ if (!_.isNil(user.email)) row.email = user.email;
137
+ if (!_.isNil(user.phone)) row.phone = user.phone;
138
+ if (user.newPassword)
139
+ row.password = bcrypt.hashSync(user.newPassword, 10);
140
+ if (typeof user.totpEnabled == 'boolean')
141
+ row.totpEnabled = user.totpEnabled;
142
+
143
+ payload.id = row._id;
144
+ payload.username = row.username;
145
+ payload.role = row.role;
146
+ payload.firstname = row.firstname;
147
+ payload.lastname = row.lastname;
148
+ payload.email = row.email;
149
+ payload.phone = row.phone;
150
+ payload.roles = auth.acl.getRoles(payload.role);
151
+
152
+ return row.save();
153
+ } else
154
+ throw { fn: 'Unauthorized', message: 'Current password is invalid' };
155
+ })
156
+ .then(function () {
157
+ var token = jwt.sign(payload, auth.jwtSecret, {
158
+ expiresIn: '15 minutes',
159
+ });
160
+ resolve({ token: `JWT ${token}` });
161
+ })
162
+ .catch(function (err) {
163
+ if (err.code === 11000)
164
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
165
+ else reject(err);
166
+ });
167
+ });
168
+ };
169
+
170
+ // Update user (for admin usage)
171
+ UserSchema.statics.updateUser = function (userId, user) {
172
+ return new Promise((resolve, reject) => {
173
+ if (user.password) user.password = bcrypt.hashSync(user.password, 10);
174
+ var query = this.findOneAndUpdate({ _id: userId }, user);
175
+ query
176
+ .exec()
177
+ .then(function (row) {
178
+ if (row) resolve('User updated successfully');
179
+ else reject({ fn: 'NotFound', message: 'User not found' });
180
+ })
181
+ .catch(function (err) {
182
+ if (err.code === 11000)
183
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
184
+ else reject(err);
185
+ });
186
+ });
187
+ };
188
+
189
+ // Update refreshtoken
190
+ UserSchema.statics.updateRefreshToken = function (refreshToken, userAgent) {
191
+ return new Promise((resolve, reject) => {
192
+ var token = '';
193
+ var newRefreshToken = '';
194
+ try {
195
+ var decoded = jwt.verify(refreshToken, auth.jwtRefreshSecret);
196
+ var userId = decoded.userId;
197
+ var sessionId = decoded.sessionId;
198
+ var expiration = decoded.exp;
199
+ } catch (err) {
200
+ if (err.name === 'TokenExpiredError')
201
+ throw { fn: 'Unauthorized', message: 'Expired refreshToken' };
202
+ else throw { fn: 'Unauthorized', message: 'Invalid refreshToken' };
203
+ }
204
+ var query = this.findById(userId);
205
+ query
206
+ .exec()
207
+ .then(row => {
208
+ if (row && row.enabled !== false) {
209
+ // Check session exist and sessionId not null (if null then it is a login)
210
+ if (sessionId !== null) {
211
+ var sessionExist = row.refreshTokens.findIndex(
212
+ e => e.sessionId === sessionId && e.token === refreshToken,
213
+ );
214
+ if (sessionExist === -1)
215
+ // Not found
216
+ throw { fn: 'Unauthorized', message: 'Session not found' };
217
+ }
218
+
219
+ // Generate new token
220
+ var payload = {};
221
+ payload.id = row._id;
222
+ payload.username = row.username;
223
+ payload.role = row.role;
224
+ payload.firstname = row.firstname;
225
+ payload.lastname = row.lastname;
226
+ payload.email = row.email;
227
+ payload.phone = row.phone;
228
+ payload.roles = auth.acl.getRoles(payload.role);
229
+
230
+ token = jwt.sign(payload, auth.jwtSecret, {
231
+ expiresIn: '15 minutes',
232
+ });
233
+
234
+ // Remove expired sessions
235
+ row.refreshTokens = row.refreshTokens.filter(e => {
236
+ try {
237
+ var decoded = jwt.verify(e.token, auth.jwtRefreshSecret);
238
+ } catch (err) {
239
+ var decoded = null;
240
+ }
241
+ return decoded !== null;
242
+ });
243
+ // Update or add new refresh token
244
+ var foundIndex = row.refreshTokens.findIndex(
245
+ e => e.sessionId === sessionId,
246
+ );
247
+ if (foundIndex === -1) {
248
+ // Not found
249
+ sessionId = generateUUID();
250
+ newRefreshToken = jwt.sign(
251
+ { sessionId: sessionId, userId: userId },
252
+ auth.jwtRefreshSecret,
253
+ { expiresIn: '7 days' },
254
+ );
255
+ row.refreshTokens.push({
256
+ sessionId: sessionId,
257
+ userAgent: userAgent,
258
+ token: newRefreshToken,
259
+ });
260
+ } else {
261
+ newRefreshToken = jwt.sign(
262
+ { sessionId: sessionId, userId: userId, exp: expiration },
263
+ auth.jwtRefreshSecret,
264
+ );
265
+ row.refreshTokens[foundIndex].token = newRefreshToken;
266
+ }
267
+ return row.save();
268
+ } else if (row) {
269
+ reject({ fn: 'Unauthorized', message: 'Account disabled' });
270
+ } else reject({ fn: 'NotFound', message: 'Session not found' });
271
+ })
272
+ .then(() => {
273
+ resolve({ token: token, refreshToken: newRefreshToken });
274
+ })
275
+ .catch(err => {
276
+ if (err.code === 11000)
277
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
278
+ else reject(err);
279
+ });
280
+ });
281
+ };
282
+
283
+ // Remove session
284
+ UserSchema.statics.removeSession = function (userId, sessionId) {
285
+ return new Promise((resolve, reject) => {
286
+ var query = this.findById(userId);
287
+ query
288
+ .exec()
289
+ .then(row => {
290
+ if (row) {
291
+ row.refreshTokens = row.refreshTokens.filter(
292
+ e => e.sessionId !== sessionId,
293
+ );
294
+ return row.save();
295
+ } else reject({ fn: 'NotFound', message: 'User not found' });
296
+ })
297
+ .then(() => {
298
+ resolve('Session removed successfully');
299
+ })
300
+ .catch(err => {
301
+ if (err.code === 11000)
302
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
303
+ else reject(err);
304
+ });
305
+ });
306
+ };
307
+
308
+ // gen totp QRCode url
309
+ UserSchema.statics.getTotpQrcode = function (username) {
310
+ return new Promise((resolve, reject) => {
311
+ let newConfig = totpConfig;
312
+ newConfig.label = username;
313
+ const secret = new OTPAuth.Secret({
314
+ size: 10,
315
+ }).base32;
316
+ newConfig.secret = secret;
317
+
318
+ let totp = new OTPAuth.TOTP(newConfig);
319
+ let totpUrl = totp.toString();
320
+
321
+ QRCode.toDataURL(totpUrl, function (err, url) {
322
+ resolve({
323
+ totpQrCode: url,
324
+ totpSecret: secret,
325
+ });
326
+ });
327
+ });
328
+ };
329
+
330
+ // verify TOTP and Setup enabled status and secret code
331
+ UserSchema.statics.setupTotp = function (token, secret, username) {
332
+ return new Promise((resolve, reject) => {
333
+ checkTotpToken(token, secret);
334
+
335
+ var query = this.findOne({ username: username });
336
+ query
337
+ .exec()
338
+ .then(function (row) {
339
+ if (!row) throw { errmsg: 'User not found' };
340
+ else if (row.totpEnabled === true)
341
+ throw { errmsg: 'TOTP already enabled by this user' };
342
+ else {
343
+ row.totpEnabled = true;
344
+ row.totpSecret = secret;
345
+ return row.save();
346
+ }
347
+ })
348
+ .then(function () {
349
+ resolve({ msg: true });
350
+ })
351
+ .catch(function (err) {
352
+ reject(err);
353
+ });
354
+ });
355
+ };
356
+
357
+ // verify TOTP and Cancel enabled status and secret code
358
+ UserSchema.statics.cancelTotp = function (token, username) {
359
+ return new Promise((resolve, reject) => {
360
+ var query = this.findOne({ username: username });
361
+ query
362
+ .exec()
363
+ .then(function (row) {
364
+ if (!row) throw { errmsg: 'User not found' };
365
+ else if (row.totpEnabled !== true)
366
+ throw { errmsg: 'TOTP is not enabled yet' };
367
+ else {
368
+ checkTotpToken(token, row.totpSecret);
369
+ row.totpEnabled = false;
370
+ row.totpSecret = '';
371
+ return row.save();
372
+ }
373
+ })
374
+ .then(function () {
375
+ resolve({ msg: 'TOTP is canceled.' });
376
+ })
377
+ .catch(function (err) {
378
+ reject(err);
379
+ });
380
+ });
381
+ };
382
+
383
+ /*
384
+ *** Methods ***
385
+ */
386
+
387
+ // Authenticate user with username and password, return JWT token
388
+ UserSchema.methods.getToken = function (userAgent) {
389
+ return new Promise((resolve, reject) => {
390
+ var user = this;
391
+ var query = User.findOne({ username: user.username });
392
+ query
393
+ .exec()
394
+ .then(function (row) {
395
+ if (row && row.enabled === false)
396
+ throw { fn: 'Unauthorized', message: 'Authentication Failed.' };
397
+
398
+ if (row && bcrypt.compareSync(user.password, row.password)) {
399
+ if (row.totpEnabled && user.totpToken)
400
+ checkTotpToken(user.totpToken, row.totpSecret);
401
+ else if (row.totpEnabled)
402
+ throw { fn: 'BadParameters', message: 'Missing TOTP token' };
403
+ var refreshToken = jwt.sign(
404
+ { sessionId: null, userId: row._id },
405
+ auth.jwtRefreshSecret,
406
+ );
407
+ return User.updateRefreshToken(refreshToken, userAgent);
408
+ } else {
409
+ if (!row) {
410
+ // We compare two random strings to generate delay
411
+ var randomHash =
412
+ '$2b$10$' +
413
+ [...Array(53)].map(() => Math.random().toString(36)[2]).join('');
414
+ bcrypt.compareSync(user.password, randomHash);
415
+ }
416
+
417
+ throw { fn: 'Unauthorized', message: 'Authentication Failed.' };
418
+ }
419
+ })
420
+ .then(row => {
421
+ resolve({ token: row.token, refreshToken: row.refreshToken });
422
+ })
423
+ .catch(function (err) {
424
+ reject(err);
425
+ });
426
+ });
427
+ };
428
+
429
+ var User = mongoose.model('User', UserSchema);
430
+ module.exports = User;
backend/src/models/vulnerability-category.js ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var VulnerabilityCategorySchema = new Schema(
5
+ {
6
+ name: { type: String, unique: true },
7
+ sortValue: { type: String, default: 'cvssScore' },
8
+ sortOrder: { type: String, enum: ['desc', 'asc'], default: 'desc' },
9
+ sortAuto: { type: Boolean, default: true },
10
+ },
11
+ { timestamps: true },
12
+ );
13
+
14
+ /*
15
+ *** Statics ***
16
+ */
17
+
18
+ // Get all vulnerabilityCategorys
19
+ VulnerabilityCategorySchema.statics.getAll = () => {
20
+ return new Promise((resolve, reject) => {
21
+ var query = VulnerabilityCategory.find();
22
+ query.select('name sortValue sortOrder sortAuto');
23
+ query
24
+ .exec()
25
+ .then(rows => {
26
+ resolve(rows);
27
+ })
28
+ .catch(err => {
29
+ reject(err);
30
+ });
31
+ });
32
+ };
33
+
34
+ // Create vulnerabilityCategory
35
+ VulnerabilityCategorySchema.statics.create = vulnerabilityCategory => {
36
+ return new Promise((resolve, reject) => {
37
+ var query = new VulnerabilityCategory(vulnerabilityCategory);
38
+ query
39
+ .save()
40
+ .then(row => {
41
+ resolve(row);
42
+ })
43
+ .catch(err => {
44
+ if (err.code === 11000)
45
+ reject({
46
+ fn: 'BadParameters',
47
+ message: 'Vulnerability Category name already exists',
48
+ });
49
+ else reject(err);
50
+ });
51
+ });
52
+ };
53
+
54
+ // Update vulnerabilityCategory
55
+ VulnerabilityCategorySchema.statics.update = (name, vulnerabilityCategory) => {
56
+ return new Promise((resolve, reject) => {
57
+ var query = VulnerabilityCategory.findOneAndUpdate(
58
+ { name: name },
59
+ vulnerabilityCategory,
60
+ );
61
+ query
62
+ .exec()
63
+ .then(row => {
64
+ if (row) resolve(row);
65
+ else
66
+ reject({
67
+ fn: 'NotFound',
68
+ message: 'Vulnerability category not found',
69
+ });
70
+ })
71
+ .catch(err => {
72
+ if (err.code === 11000)
73
+ reject({
74
+ fn: 'BadParameters',
75
+ message: 'Vulnerability Category already exists',
76
+ });
77
+ else reject(err);
78
+ });
79
+ });
80
+ };
81
+
82
+ // Update vulnerability Categories
83
+ VulnerabilityCategorySchema.statics.updateAll = vulnCategories => {
84
+ return new Promise((resolve, reject) => {
85
+ VulnerabilityCategory.deleteMany()
86
+ .then(row => {
87
+ VulnerabilityCategory.insertMany(vulnCategories);
88
+ })
89
+ .then(row => {
90
+ resolve('Vulnerability Categories updated successfully');
91
+ })
92
+ .catch(err => {
93
+ reject(err);
94
+ });
95
+ });
96
+ };
97
+
98
+ // Delete vulnerabilityCategory
99
+ VulnerabilityCategorySchema.statics.delete = name => {
100
+ return new Promise((resolve, reject) => {
101
+ VulnerabilityCategory.deleteOne({ name: name })
102
+ .then(res => {
103
+ if (res.deletedCount === 1) resolve('Vulnerability Category deleted');
104
+ else
105
+ reject({
106
+ fn: 'NotFound',
107
+ message: 'Vulnerability Category not found',
108
+ });
109
+ })
110
+ .catch(err => {
111
+ reject(err);
112
+ });
113
+ });
114
+ };
115
+
116
+ /*
117
+ *** Methods ***
118
+ */
119
+
120
+ var VulnerabilityCategory = mongoose.model(
121
+ 'VulnerabilityCategory',
122
+ VulnerabilityCategorySchema,
123
+ );
124
+ module.exports = VulnerabilityCategory;
backend/src/models/vulnerability-type.js ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var VulnerabilityTypeSchema = new Schema(
5
+ {
6
+ name: String,
7
+ locale: String,
8
+ },
9
+ { timestamps: true },
10
+ );
11
+
12
+ VulnerabilityTypeSchema.index(
13
+ { name: 1, locale: 1 },
14
+ { name: 'unique_name_locale', unique: true },
15
+ );
16
+
17
+ /*
18
+ *** Statics ***
19
+ */
20
+
21
+ // Get all vulnerabilityTypes
22
+ VulnerabilityTypeSchema.statics.getAll = () => {
23
+ return new Promise((resolve, reject) => {
24
+ var query = VulnerabilityType.find();
25
+ query.select('-_id name locale');
26
+ query
27
+ .exec()
28
+ .then(rows => {
29
+ resolve(rows);
30
+ })
31
+ .catch(err => {
32
+ reject(err);
33
+ });
34
+ });
35
+ };
36
+
37
+ // Create vulnerabilityType
38
+ VulnerabilityTypeSchema.statics.create = vulnerabilityType => {
39
+ return new Promise((resolve, reject) => {
40
+ var query = new VulnerabilityType(vulnerabilityType);
41
+ query
42
+ .save()
43
+ .then(row => {
44
+ resolve(row);
45
+ })
46
+ .catch(err => {
47
+ if (err.code === 11000)
48
+ reject({
49
+ fn: 'BadParameters',
50
+ message: 'Vulnerability Type already exists',
51
+ });
52
+ else reject(err);
53
+ });
54
+ });
55
+ };
56
+
57
+ // Update vulnerability Types
58
+ VulnerabilityTypeSchema.statics.updateAll = vulnerabilityTypes => {
59
+ return new Promise((resolve, reject) => {
60
+ VulnerabilityType.deleteMany()
61
+ .then(row => {
62
+ VulnerabilityType.insertMany(vulnerabilityTypes);
63
+ })
64
+ .then(row => {
65
+ resolve('Vulnerability Types updated successfully');
66
+ })
67
+ .catch(err => {
68
+ reject(err);
69
+ });
70
+ });
71
+ };
72
+
73
+ // Delete vulnerabilityType
74
+ VulnerabilityTypeSchema.statics.delete = name => {
75
+ return new Promise((resolve, reject) => {
76
+ VulnerabilityType.deleteOne({ name: name })
77
+ .then(res => {
78
+ if (res.deletedCount === 1) resolve('Vulnerability Type deleted');
79
+ else
80
+ reject({ fn: 'NotFound', message: 'Vulnerability Type not found' });
81
+ })
82
+ .catch(err => {
83
+ reject(err);
84
+ });
85
+ });
86
+ };
87
+
88
+ /*
89
+ *** Methods ***
90
+ */
91
+
92
+ var VulnerabilityType = mongoose.model(
93
+ 'VulnerabilityType',
94
+ VulnerabilityTypeSchema,
95
+ );
96
+ module.exports = VulnerabilityType;
backend/src/models/vulnerability-update.js ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var _ = require('lodash');
3
+
4
+ var Schema = mongoose.Schema;
5
+
6
+ var customField = {
7
+ _id: false,
8
+ customField: { type: Schema.Types.ObjectId, ref: 'CustomField' },
9
+ text: Schema.Types.Mixed,
10
+ };
11
+
12
+ var VulnerabilityUpdateSchema = new Schema(
13
+ {
14
+ vulnerability: {
15
+ type: Schema.Types.ObjectId,
16
+ ref: 'Vulnerability',
17
+ required: true,
18
+ },
19
+ creator: { type: Schema.Types.ObjectId, ref: 'User', required: true },
20
+ cvssv3: String,
21
+ priority: { type: Number, enum: [1, 2, 3, 4] },
22
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
23
+ references: [String],
24
+ locale: String,
25
+ title: String,
26
+ vulnType: String,
27
+ description: String,
28
+ observation: String,
29
+ remediation: String,
30
+ category: String,
31
+ customFields: [customField],
32
+ },
33
+ { timestamps: true },
34
+ );
35
+
36
+ /*
37
+ *** Statics ***
38
+ */
39
+
40
+ // Get all vulnerabilities
41
+ VulnerabilityUpdateSchema.statics.getAll = () => {
42
+ return new Promise((resolve, reject) => {
43
+ var query = VulnerabilityUpdate.find();
44
+ query
45
+ .exec()
46
+ .then(rows => {
47
+ resolve(rows);
48
+ })
49
+ .catch(err => {
50
+ reject(err);
51
+ });
52
+ });
53
+ };
54
+
55
+ // Get all updates of vulnerability
56
+ VulnerabilityUpdateSchema.statics.getAllByVuln = vulnId => {
57
+ return new Promise((resolve, reject) => {
58
+ var query = VulnerabilityUpdate.find({ vulnerability: vulnId });
59
+ query.populate('creator', '-_id username');
60
+ query.populate('customFields.customField', 'fieldType label');
61
+ query
62
+ .exec()
63
+ .then(rows => {
64
+ resolve(rows);
65
+ })
66
+ .catch(err => {
67
+ reject(err);
68
+ });
69
+ });
70
+ };
71
+
72
+ // Create vulnerability
73
+ VulnerabilityUpdateSchema.statics.create = (username, vulnerability) => {
74
+ return new Promise((resolve, reject) => {
75
+ var created = true;
76
+ var User = mongoose.model('User');
77
+ var creator = '';
78
+ var Vulnerability = mongoose.model('Vulnerability');
79
+ var query = User.findOne({ username: username });
80
+ query
81
+ .exec()
82
+ .then(row => {
83
+ if (row) {
84
+ creator = row._id;
85
+ var query = Vulnerability.findOne({
86
+ 'details.title': vulnerability.title,
87
+ });
88
+ return query.exec();
89
+ } else throw { fn: 'NotFound', message: 'User not found' };
90
+ })
91
+ .then(row => {
92
+ if (row) {
93
+ if (row.status === 1)
94
+ throw {
95
+ fn: 'Forbidden',
96
+ message: 'Vulnerability not approved yet',
97
+ };
98
+ else {
99
+ // Check if there are any changes from the original vulnerability
100
+ var detail = row.details.find(
101
+ d => d.locale === vulnerability.locale,
102
+ );
103
+ // console.log(vulnerability.customFields)
104
+ // console.log(detail.customFields)
105
+ if (
106
+ typeof detail !== 'undefined' &&
107
+ (row.cvssv3 || '').includes(vulnerability.cvssv3) &&
108
+ vulnerability.priority === (row.priority || null) &&
109
+ vulnerability.remediationComplexity ===
110
+ (row.remediationComplexity || null) &&
111
+ _.isEqual(vulnerability.references, detail.references || []) &&
112
+ vulnerability.category === (row.category || null) &&
113
+ vulnerability.vulnType === (detail.vulnType || null) &&
114
+ vulnerability.description === (detail.description || null) &&
115
+ vulnerability.observation === (detail.observation || null) &&
116
+ vulnerability.remediation === (detail.remediation || null) &&
117
+ vulnerability.customFields.length ===
118
+ detail.customFields.length &&
119
+ vulnerability.customFields.every((e, idx) => {
120
+ return (
121
+ e.customField._id == detail.customFields[idx].customField &&
122
+ e.text === detail.customFields[idx].text
123
+ );
124
+ })
125
+ ) {
126
+ throw {
127
+ fn: 'BadParameters',
128
+ message: 'No changes from the original vulnerability',
129
+ };
130
+ }
131
+ vulnerability.vulnerability = row._id;
132
+ vulnerability.creator = creator;
133
+ var query = new VulnerabilityUpdate(vulnerability);
134
+ created = false;
135
+ return query.save();
136
+ }
137
+ } else {
138
+ var vuln = {};
139
+ vuln.cvssv3 = vulnerability.cvssv3 || null;
140
+ vuln.priority = vulnerability.priority || null;
141
+ vuln.remediationComplexity =
142
+ vulnerability.remediationComplexity || null;
143
+ vuln.category = vulnerability.category || null;
144
+ vuln.creator = creator;
145
+ var details = {};
146
+ details.locale = vulnerability.locale || null;
147
+ details.title = vulnerability.title || null;
148
+ details.vulnType = vulnerability.vulnType || null;
149
+ details.description = vulnerability.description || null;
150
+ details.observation = vulnerability.observation || null;
151
+ details.remediation = vulnerability.remediation || null;
152
+ details.references = vulnerability.references || null;
153
+ details.customFields = vulnerability.customFields || [];
154
+ vuln.details = [details];
155
+ var query = new Vulnerability(vuln);
156
+ return query.save();
157
+ }
158
+ })
159
+ .then(row => {
160
+ if (created) resolve('Finding created as new Vulnerability');
161
+ else {
162
+ var query = Vulnerability.findOneAndUpdate(
163
+ { 'details.title': vulnerability.title },
164
+ { status: 2 },
165
+ );
166
+ return query.exec();
167
+ }
168
+ })
169
+ .then(row => {
170
+ resolve('Update proposed for existing vulnerability');
171
+ })
172
+ .catch(err => {
173
+ reject(err);
174
+ });
175
+ });
176
+ };
177
+
178
+ VulnerabilityUpdateSchema.statics.deleteAllByVuln = async vulnId => {
179
+ return await VulnerabilityUpdate.deleteMany({ vulnerability: vulnId });
180
+ };
181
+
182
+ /*
183
+ *** Methods ***
184
+ */
185
+
186
+ var VulnerabilityUpdate = mongoose.model(
187
+ 'VulnerabilityUpdate',
188
+ VulnerabilityUpdateSchema,
189
+ );
190
+ module.exports = VulnerabilityUpdate;
backend/src/models/vulnerability.js ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var mongoose = require('mongoose');
2
+ var Schema = mongoose.Schema;
3
+
4
+ var customField = {
5
+ _id: false,
6
+ customField: { type: Schema.Types.ObjectId, ref: 'CustomField' },
7
+ text: Schema.Types.Mixed,
8
+ };
9
+
10
+ var VulnerabilityDetails = {
11
+ _id: false,
12
+ locale: String,
13
+ // language: String,
14
+ title: { type: String, unique: true, sparse: true },
15
+ vulnType: String,
16
+ description: String,
17
+ observation: String,
18
+ remediation: String,
19
+ cwes: [String],
20
+ references: [String],
21
+ customFields: [customField],
22
+ };
23
+
24
+ var VulnerabilitySchema = new Schema(
25
+ {
26
+ cvssv3: String,
27
+ priority: { type: Number, enum: [1, 2, 3, 4] },
28
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
29
+ details: [VulnerabilityDetails],
30
+ status: { type: Number, enum: [0, 1, 2], default: 1 }, // 0: validated, 1: created, 2: updated,
31
+ category: String,
32
+ creator: { type: Schema.Types.ObjectId, ref: 'User' },
33
+ },
34
+ { timestamps: true },
35
+ );
36
+
37
+ /*
38
+ *** Statics ***
39
+ */
40
+
41
+ // Get all vulnerabilities
42
+ VulnerabilitySchema.statics.getAll = () => {
43
+ return new Promise((resolve, reject) => {
44
+ var query = Vulnerability.find();
45
+ query.populate('creator', '-_id username');
46
+ query
47
+ .exec()
48
+ .then(rows => {
49
+ resolve(rows);
50
+ })
51
+ .catch(err => {
52
+ reject(err);
53
+ });
54
+ });
55
+ };
56
+
57
+ // Get all vulnerabilities for download
58
+ VulnerabilitySchema.statics.export = () => {
59
+ return new Promise((resolve, reject) => {
60
+ var query = Vulnerability.find();
61
+ query.select(
62
+ 'details cvssv3 priority remediationComplexity cwes references category -_id',
63
+ );
64
+ query
65
+ .exec()
66
+ .then(rows => {
67
+ resolve(rows);
68
+ })
69
+ .catch(err => {
70
+ reject(err);
71
+ });
72
+ });
73
+ };
74
+
75
+ // Create vulnerability
76
+ VulnerabilitySchema.statics.create = vulnerabilities => {
77
+ return new Promise((resolve, reject) => {
78
+ Vulnerability.insertMany(vulnerabilities, { ordered: false })
79
+ .then(rows => {
80
+ resolve({ created: rows.length, duplicates: 0 });
81
+ })
82
+ .catch(err => {
83
+ if (err.code === 11000) {
84
+ if (err.result.nInserted === 0)
85
+ reject({
86
+ fn: 'BadParameters',
87
+ message: 'Vulnerability title already exists',
88
+ });
89
+ else {
90
+ var errorMessages = [];
91
+ err.writeErrors.forEach(e =>
92
+ errorMessages.push(e.errmsg || 'no errmsg'),
93
+ );
94
+ resolve({
95
+ created: err.result.nInserted,
96
+ duplicates: errorMessages,
97
+ });
98
+ }
99
+ } else reject(err);
100
+ });
101
+ });
102
+ };
103
+
104
+ // Update vulnerability
105
+ VulnerabilitySchema.statics.update = (vulnerabilityId, vulnerability) => {
106
+ return new Promise((resolve, reject) => {
107
+ var VulnerabilityUpdate = mongoose.model('VulnerabilityUpdate');
108
+ var query = Vulnerability.findByIdAndUpdate(vulnerabilityId, vulnerability);
109
+ query
110
+ .exec()
111
+ .then(row => {
112
+ if (!row)
113
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
114
+ else {
115
+ var query = VulnerabilityUpdate.deleteMany({
116
+ vulnerability: vulnerabilityId,
117
+ });
118
+ return query.exec();
119
+ }
120
+ })
121
+ .then(row => {
122
+ resolve('Vulnerability updated successfully');
123
+ })
124
+ .catch(err => {
125
+ if (err.code === 11000)
126
+ reject({
127
+ fn: 'BadParameters',
128
+ message: 'Vulnerability title already exists',
129
+ });
130
+ else reject(err);
131
+ });
132
+ });
133
+ };
134
+
135
+ // Delete all vulnerabilities
136
+ VulnerabilitySchema.statics.deleteAll = () => {
137
+ return new Promise((resolve, reject) => {
138
+ var query = Vulnerability.deleteMany();
139
+ query
140
+ .exec()
141
+ .then(() => {
142
+ resolve('All vulnerabilities deleted successfully');
143
+ })
144
+ .catch(err => {
145
+ reject(err);
146
+ });
147
+ });
148
+ };
149
+
150
+ // Delete vulnerability
151
+ VulnerabilitySchema.statics.delete = vulnerabilityId => {
152
+ return new Promise((resolve, reject) => {
153
+ var query = Vulnerability.findByIdAndDelete(vulnerabilityId);
154
+ query
155
+ .exec()
156
+ .then(rows => {
157
+ if (rows) resolve(rows);
158
+ else reject({ fn: 'NotFound', message: 'Vulnerability not found' });
159
+ })
160
+ .catch(err => {
161
+ reject(err);
162
+ });
163
+ });
164
+ };
165
+
166
+ // Get vulnerabilities by language
167
+ VulnerabilitySchema.statics.getAllByLanguage = locale => {
168
+ return new Promise((resolve, reject) => {
169
+ var query = Vulnerability.find({ 'details.locale': locale });
170
+ query.select('details cvssv3 priority remediationComplexity category');
171
+ query
172
+ .exec()
173
+ .then(rows => {
174
+ if (rows) {
175
+ var result = [];
176
+ rows.forEach(row => {
177
+ row.details.forEach(detail => {
178
+ if (detail.locale === locale && detail.title) {
179
+ var temp = {};
180
+ temp.cvssv3 = row.cvssv3;
181
+ temp.priority = row.priority;
182
+ temp.remediationComplexity = row.remediationComplexity;
183
+ temp.category = row.category;
184
+ temp.detail = detail;
185
+ temp._id = row._id;
186
+ result.push(temp);
187
+ }
188
+ });
189
+ });
190
+ resolve(result);
191
+ } else
192
+ reject({
193
+ fn: 'NotFound',
194
+ message: 'Locale with existing title not found',
195
+ });
196
+ })
197
+ .catch(err => {
198
+ reject(err);
199
+ });
200
+ });
201
+ };
202
+
203
+ VulnerabilitySchema.statics.Merge = (vulnIdPrime, vulnIdMerge, locale) => {
204
+ return new Promise((resolve, reject) => {
205
+ var mergeDetail = null;
206
+ var mergeVuln = null;
207
+ var primeVuln = null;
208
+ var query = Vulnerability.findById(vulnIdMerge);
209
+ query
210
+ .exec()
211
+ .then(row => {
212
+ if (!row)
213
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
214
+ else {
215
+ mergeVuln = row;
216
+ mergeDetail = row.details.find(d => d.locale === locale);
217
+ var query = Vulnerability.findById(vulnIdPrime);
218
+ return query.exec();
219
+ }
220
+ })
221
+ .then(row => {
222
+ if (!row)
223
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
224
+ else {
225
+ if (row.details.findIndex(d => d.locale === locale && d.title) !== -1)
226
+ reject({
227
+ fn: 'BadParameters',
228
+ message: 'Language already exists in this vulnerability',
229
+ });
230
+ else {
231
+ primeVuln = row;
232
+ var removeIndex = mergeVuln.details
233
+ .map(d => d.title)
234
+ .indexOf(mergeDetail.title);
235
+ mergeVuln.details.splice(removeIndex, 1);
236
+ if (mergeVuln.details.length === 0)
237
+ return Vulnerability.findByIdAndDelete(mergeVuln._id);
238
+ else return mergeVuln.save();
239
+ }
240
+ }
241
+ })
242
+ .then(() => {
243
+ var detail = {};
244
+ detail.locale = mergeDetail.locale;
245
+ detail.title = mergeDetail.title;
246
+ if (mergeDetail.vulnType) detail.vulnType = mergeDetail.vulnType;
247
+ if (mergeDetail.description)
248
+ detail.description = mergeDetail.description;
249
+ if (mergeDetail.observation)
250
+ detail.observation = mergeDetail.observation;
251
+ if (mergeDetail.remediation)
252
+ detail.remediation = mergeDetail.remediation;
253
+ if (mergeDetail.customFields)
254
+ detail.customFields = mergeDetail.customFields;
255
+ primeVuln.details.push(detail);
256
+ return primeVuln.save();
257
+ })
258
+ .then(() => {
259
+ resolve('Vulnerability merge successfull');
260
+ })
261
+ .catch(err => {
262
+ reject(err);
263
+ });
264
+ });
265
+ };
266
+
267
+ /*
268
+ *** Methods ***
269
+ */
270
+
271
+ var Vulnerability = mongoose.model('Vulnerability', VulnerabilitySchema);
272
+ module.exports = Vulnerability;
backend/src/routes/audit.js ADDED
@@ -0,0 +1,1168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = function (app, io) {
2
+ var Response = require('../lib/httpResponse');
3
+ var Audit = require('mongoose').model('Audit');
4
+ var acl = require('../lib/auth').acl;
5
+ var reportGenerator = require('../lib/report-generator');
6
+ var _ = require('lodash');
7
+ var utils = require('../lib/utils');
8
+ var Settings = require('mongoose').model('Settings');
9
+
10
+ /* ### AUDITS LIST ### */
11
+
12
+ // Get audits list of user (all for admin) with regex filter on findings
13
+ app.get('/api/audits', acl.hasPermission('audits:read'), function (req, res) {
14
+ var getUsersRoom = function (room) {
15
+ return utils.getSockets(io, room).map(s => s.username);
16
+ };
17
+ var filters = {};
18
+ if (req.query.findingTitle)
19
+ filters['findings.title'] = new RegExp(
20
+ utils.escapeRegex(req.query.findingTitle),
21
+ 'i',
22
+ );
23
+ if (req.query.type && req.query.type === 'default')
24
+ filters.$or = [{ type: 'default' }, { type: { $exists: false } }];
25
+ if (req.query.type && ['multi', 'retest'].includes(req.query.type))
26
+ filters.type = req.query.type;
27
+
28
+ Audit.getAudits(
29
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
30
+ req.decodedToken.id,
31
+ filters,
32
+ )
33
+ .then(msg => {
34
+ var result = [];
35
+ msg.forEach(audit => {
36
+ var a = {};
37
+ a._id = audit._id;
38
+ a.name = audit.name;
39
+ a.language = audit.language;
40
+ a.auditType = audit.auditType;
41
+ a.creator = audit.creator;
42
+ a.collaborators = audit.collaborators;
43
+ a.company = audit.company;
44
+ a.createdAt = audit.createdAt;
45
+ a.ext =
46
+ !!audit.template && !!audit.template.ext
47
+ ? audit.template.ext
48
+ : 'Template error';
49
+ a.reviewers = audit.reviewers;
50
+ a.approvals = audit.approvals;
51
+ a.state = audit.state;
52
+ a.type = audit.type;
53
+ a.parentId = audit.parentId;
54
+ if (acl.isAllowed(req.decodedToken.role, 'audits:users-connected')) {
55
+ a.connected = getUsersRoom(audit._id.toString());
56
+ }
57
+ result.push(a);
58
+ });
59
+ Response.Ok(res, result);
60
+ })
61
+ .catch(err => Response.Internal(res, err));
62
+ });
63
+
64
+ // Create audit (default or multi) with name, auditType, language provided
65
+ // parentId can be set only if type is default
66
+ app.post(
67
+ '/api/audits',
68
+ acl.hasPermission('audits:create'),
69
+ function (req, res) {
70
+ if (!req.body.name || !req.body.language || !req.body.auditType) {
71
+ Response.BadParameters(
72
+ res,
73
+ 'Missing some required parameters: name, language, auditType',
74
+ );
75
+ return;
76
+ }
77
+
78
+ if (!utils.validFilename(req.body.language)) {
79
+ Response.BadParameters(res, 'Invalid characters for language');
80
+ return;
81
+ }
82
+
83
+ var audit = {};
84
+ // Required params
85
+ audit.name = req.body.name;
86
+ audit.language = req.body.language;
87
+ audit.auditType = req.body.auditType;
88
+ audit.type = 'default';
89
+
90
+ // Optional params
91
+ if (req.body.type && req.body.type === 'multi')
92
+ audit.type = req.body.type;
93
+ if (audit.type === 'default' && req.body.parentId)
94
+ audit.parentId = req.body.parentId;
95
+
96
+ Audit.create(audit, req.decodedToken.id)
97
+ .then(inserted =>
98
+ Response.Created(res, {
99
+ message: 'Audit created successfully',
100
+ audit: inserted,
101
+ }),
102
+ )
103
+ .catch(err => Response.Internal(res, err));
104
+ },
105
+ );
106
+
107
+ // Get audits children
108
+ app.get(
109
+ '/api/audits/:auditId/children',
110
+ acl.hasPermission('audits:read'),
111
+ function (req, res) {
112
+ var getUsersRoom = function (room) {
113
+ return utils.getSockets(io, room).map(s => s.username);
114
+ };
115
+ Audit.getAuditChildren(
116
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
117
+ req.params.auditId,
118
+ req.decodedToken.id,
119
+ )
120
+ .then(msg => {
121
+ var result = [];
122
+ msg.forEach(audit => {
123
+ var a = {};
124
+ a._id = audit._id;
125
+ a.name = audit.name;
126
+ a.auditType = audit.auditType;
127
+ a.approvals = audit.approvals;
128
+ a.state = audit.state;
129
+ if (
130
+ acl.isAllowed(req.decodedToken.role, 'audits:users-connected')
131
+ ) {
132
+ a.connected = getUsersRoom(audit._id.toString());
133
+ }
134
+ result.push(a);
135
+ });
136
+ Response.Ok(res, result);
137
+ })
138
+ .catch(err => Response.Internal(res, err));
139
+ },
140
+ );
141
+
142
+ // Get audit retest with auditId
143
+ app.get(
144
+ '/api/audits/:auditId/retest',
145
+ acl.hasPermission('audits:read'),
146
+ function (req, res) {
147
+ Audit.getRetest(
148
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
149
+ req.params.auditId,
150
+ req.decodedToken.id,
151
+ )
152
+ .then(msg => Response.Ok(res, msg))
153
+ .catch(err => Response.Internal(res, err));
154
+ },
155
+ );
156
+
157
+ // Create audit retest with auditId
158
+ app.post(
159
+ '/api/audits/:auditId/retest',
160
+ acl.hasPermission('audits:create'),
161
+ function (req, res) {
162
+ if (!req.body.auditType) {
163
+ Response.BadParameters(
164
+ res,
165
+ 'Missing some required parameters: auditType',
166
+ );
167
+ return;
168
+ }
169
+ Audit.createRetest(
170
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
171
+ req.params.auditId,
172
+ req.decodedToken.id,
173
+ req.body.auditType,
174
+ )
175
+ .then(inserted =>
176
+ Response.Created(res, {
177
+ message: 'Audit Retest created successfully',
178
+ audit: inserted,
179
+ }),
180
+ )
181
+ .catch(err => Response.Internal(res, err));
182
+ },
183
+ );
184
+
185
+ // Delete audit if creator or admin
186
+ app.delete(
187
+ '/api/audits/:auditId',
188
+ acl.hasPermission('audits:delete'),
189
+ function (req, res) {
190
+ Audit.delete(
191
+ acl.isAllowed(req.decodedToken.role, 'audits:delete-all'),
192
+ req.params.auditId,
193
+ req.decodedToken.id,
194
+ )
195
+ .then(msg => Response.Ok(res, msg))
196
+ .catch(err => Response.Internal(res, err));
197
+ },
198
+ );
199
+
200
+ /* ### AUDITS EDIT ### */
201
+
202
+ // Get Audit with ID
203
+ app.get(
204
+ '/api/audits/:auditId',
205
+ acl.hasPermission('audits:read'),
206
+ function (req, res) {
207
+ Audit.getAudit(
208
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
209
+ req.params.auditId,
210
+ req.decodedToken.id,
211
+ )
212
+ .then(msg => Response.Ok(res, msg))
213
+ .catch(err => Response.Internal(res, err));
214
+ },
215
+ );
216
+
217
+ // Get audit general information
218
+ app.get(
219
+ '/api/audits/:auditId/general',
220
+ acl.hasPermission('audits:read'),
221
+ function (req, res) {
222
+ Audit.getGeneral(
223
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
224
+ req.params.auditId,
225
+ req.decodedToken.id,
226
+ )
227
+ .then(msg => Response.Ok(res, msg))
228
+ .catch(err => Response.Internal(res, err));
229
+ },
230
+ );
231
+
232
+ // Update audit general information
233
+ app.put(
234
+ '/api/audits/:auditId/general',
235
+ acl.hasPermission('audits:update'),
236
+ async function (req, res) {
237
+ var update = {};
238
+
239
+ var settings = await Settings.getAll();
240
+ var audit = await Audit.getAudit(
241
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
242
+ req.params.auditId,
243
+ req.decodedToken.id,
244
+ );
245
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
246
+ Response.Forbidden(
247
+ res,
248
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
249
+ );
250
+ return;
251
+ }
252
+
253
+ if (req.body.reviewers) {
254
+ if (req.body.reviewers.some(element => !element._id)) {
255
+ Response.BadParameters(res, 'One or more reviewer is missing an _id');
256
+ return;
257
+ }
258
+
259
+ // Is the new reviewer the creator of the audit?
260
+ if (
261
+ req.body.reviewers.some(element => element._id === audit.creator._id)
262
+ ) {
263
+ Response.BadParameters(
264
+ res,
265
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
266
+ );
267
+ return;
268
+ }
269
+
270
+ // Is the new reviewer one of the new collaborators that will override current collaborators?
271
+ if (req.body.collaborators) {
272
+ req.body.reviewers.forEach(reviewer => {
273
+ if (
274
+ req.body.collaborators.some(
275
+ element => !element._id || element._id === reviewer._id,
276
+ )
277
+ ) {
278
+ Response.BadParameters(
279
+ res,
280
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
281
+ );
282
+ return;
283
+ }
284
+ });
285
+ }
286
+
287
+ // If no new collaborators are being set, is the new reviewer one of the current collaborators?
288
+ else if (audit.collaborators) {
289
+ req.body.reviewers.forEach(reviewer => {
290
+ if (
291
+ audit.collaborators.some(element => element._id === reviewer._id)
292
+ ) {
293
+ Response.BadParameters(
294
+ res,
295
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
296
+ );
297
+ return;
298
+ }
299
+ });
300
+ }
301
+ }
302
+
303
+ if (req.body.collaborators) {
304
+ if (req.body.collaborators.some(element => !element._id)) {
305
+ Response.BadParameters(
306
+ res,
307
+ 'One or more collaborator is missing an _id',
308
+ );
309
+ return;
310
+ }
311
+
312
+ // Are the new collaborators part of the current reviewers?
313
+ req.body.collaborators.forEach(collaborator => {
314
+ if (
315
+ audit.reviewers.some(element => element._id === collaborator._id)
316
+ ) {
317
+ Response.BadParameters(
318
+ res,
319
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
320
+ );
321
+ return;
322
+ }
323
+ });
324
+
325
+ // If the new collaborator already gave a review, remove said review, accept collaborator
326
+ if (audit.approvals) {
327
+ var newApprovals = audit.approvals.filter(
328
+ approval =>
329
+ !req.body.collaborators.some(
330
+ collaborator => approval.toString() === collaborator._id,
331
+ ),
332
+ );
333
+ update.approvals = newApprovals;
334
+ }
335
+ }
336
+
337
+ // Optional parameters
338
+ if (req.body.name) update.name = req.body.name;
339
+ if (req.body.date) update.date = req.body.date;
340
+ if (req.body.date_start) update.date_start = req.body.date_start;
341
+ if (req.body.date_end) update.date_end = req.body.date_end;
342
+ if (req.body.client !== undefined) update.client = req.body.client;
343
+ if (req.body.company !== undefined) {
344
+ update.company = {};
345
+ if (req.body.company && req.body.company._id)
346
+ update.company._id = req.body.company._id;
347
+ else if (req.body.company && req.body.company.name)
348
+ update.company.name = req.body.company.name;
349
+ else update.company = null;
350
+ }
351
+ if (req.body.collaborators) update.collaborators = req.body.collaborators;
352
+ if (req.body.reviewers) update.reviewers = req.body.reviewers;
353
+ if (req.body.language && utils.validFilename(req.body.language))
354
+ update.language = req.body.language;
355
+ if (req.body.scope && typeof (req.body.scope === 'array')) {
356
+ update.scope = req.body.scope.map(item => {
357
+ return { name: item };
358
+ });
359
+ }
360
+ if (req.body.template) update.template = req.body.template;
361
+ if (req.body.customFields) update.customFields = req.body.customFields;
362
+ if (
363
+ settings.reviews.enabled &&
364
+ settings.reviews.private.removeApprovalsUponUpdate
365
+ )
366
+ update.approvals = [];
367
+
368
+ Audit.updateGeneral(
369
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
370
+ req.params.auditId,
371
+ req.decodedToken.id,
372
+ update,
373
+ )
374
+ .then(msg => {
375
+ io.to(req.params.auditId).emit('updateAudit');
376
+ Response.Ok(res, msg);
377
+ })
378
+ .catch(err => Response.Internal(res, err));
379
+ },
380
+ );
381
+
382
+ // Get audit network information
383
+ app.get(
384
+ '/api/audits/:auditId/network',
385
+ acl.hasPermission('audits:read'),
386
+ function (req, res) {
387
+ Audit.getNetwork(
388
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
389
+ req.params.auditId,
390
+ req.decodedToken.id,
391
+ )
392
+ .then(msg => Response.Ok(res, msg))
393
+ .catch(err => Response.Internal(res, err));
394
+ },
395
+ );
396
+
397
+ // Update audit network information
398
+ app.put(
399
+ '/api/audits/:auditId/network',
400
+ acl.hasPermission('audits:update'),
401
+ async function (req, res) {
402
+ var settings = await Settings.getAll();
403
+
404
+ var audit = await Audit.getAudit(
405
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
406
+ req.params.auditId,
407
+ req.decodedToken.id,
408
+ );
409
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
410
+ Response.Forbidden(
411
+ res,
412
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
413
+ );
414
+ return;
415
+ }
416
+
417
+ var update = {};
418
+ // Optional parameters
419
+ if (req.body.scope) update.scope = req.body.scope;
420
+ if (
421
+ settings.reviews.enabled &&
422
+ settings.reviews.private.removeApprovalsUponUpdate
423
+ )
424
+ update.approvals = [];
425
+
426
+ Audit.updateNetwork(
427
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
428
+ req.params.auditId,
429
+ req.decodedToken.id,
430
+ update,
431
+ )
432
+ .then(msg => Response.Ok(res, msg))
433
+ .catch(err => Response.Internal(res, err));
434
+ },
435
+ );
436
+
437
+ // POST to export an encrypted PDF.
438
+ app.post(
439
+ '/api/audits/:auditId/generate/pdf',
440
+ acl.hasPermission('audits:read'),
441
+ function (req, res) {
442
+ Audit.getAudit(
443
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
444
+ req.params.auditId,
445
+ req.decodedToken.id,
446
+ )
447
+ .then(async audit => {
448
+ if (
449
+ ['ppt', 'pptx', 'doc', 'docx', 'docm'].includes(audit.template.ext)
450
+ ) {
451
+ let reportPdf;
452
+ if (req.body.password) {
453
+ reportPdf = await reportGenerator.generateEncryptedPdf(
454
+ audit,
455
+ req.body.password,
456
+ );
457
+ } else {
458
+ Response.BadParameters(res, 'No password included');
459
+ }
460
+
461
+ if (reportPdf) {
462
+ res.setHeader(
463
+ 'Content-Disposition',
464
+ `attachment; filename=${audit.name}.pdf`,
465
+ );
466
+ res.setHeader('Content-Type', 'application/pdf');
467
+ res.send(reportPdf);
468
+ } else {
469
+ console.error('Error generating PDF');
470
+ Response.Internal(res, 'Error generating PDF');
471
+ }
472
+ } else {
473
+ Response.BadParameters(
474
+ res,
475
+ 'Template not in a Microsoft Word/Powerpoint format',
476
+ );
477
+ }
478
+ })
479
+ .catch(err => {
480
+ console.log(err);
481
+ if (err.code === 'ENOENT')
482
+ Response.BadParameters(res, 'Template File not found');
483
+ else Response.Internal(res, err);
484
+ });
485
+ },
486
+ );
487
+
488
+ // Generate report as PDF
489
+ app.get(
490
+ '/api/audits/:auditId/generate/pdf',
491
+ acl.hasPermission('audits:read'),
492
+ function (req, res) {
493
+ Audit.getAudit(
494
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
495
+ req.params.auditId,
496
+ req.decodedToken.id,
497
+ )
498
+ .then(async audit => {
499
+ if (
500
+ ['ppt', 'pptx', 'doc', 'docx', 'docm'].find(
501
+ ext => ext === audit.template.ext,
502
+ )
503
+ ) {
504
+ var reportPdf = await reportGenerator.generatePdf(audit);
505
+ Response.SendFile(res, `${audit.name}.pdf`, reportPdf);
506
+ } else {
507
+ Response.BadParameters(
508
+ res,
509
+ 'Template not in a Microsoft Word/Powerpoint format',
510
+ );
511
+ }
512
+ })
513
+ .catch(err => {
514
+ console.log(err);
515
+ if (err.code === 'ENOENT')
516
+ Response.BadParameters(res, 'Template File not found');
517
+ else Response.Internal(res, err);
518
+ });
519
+ },
520
+ );
521
+
522
+ // Generate Report as csv
523
+ app.get(
524
+ '/api/audits/:auditId/generate/csv',
525
+ acl.hasPermission('audits:read'),
526
+ function (req, res) {
527
+ Audit.getAudit(
528
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
529
+ req.params.auditId,
530
+ req.decodedToken.id,
531
+ )
532
+ .then(async audit => {
533
+ var reportCsv = await reportGenerator.generateCsv(audit);
534
+ Response.SendFile(res, `${audit.name}.csv`, reportCsv);
535
+ })
536
+ .catch(err => {
537
+ console.log(err);
538
+ Response.Internal(res, err);
539
+ });
540
+ },
541
+ );
542
+
543
+ // Generate Report as json
544
+ app.get(
545
+ '/api/audits/:auditId/generate/json',
546
+ acl.hasPermission('audits:read'),
547
+ function (req, res) {
548
+ Audit.getAudit(
549
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
550
+ req.params.auditId,
551
+ req.decodedToken.id,
552
+ )
553
+ .then(async audit => {
554
+ Response.SendFile(res, `${audit.name}.json`, audit);
555
+ })
556
+ .catch(err => {
557
+ console.log(err);
558
+ Response.Internal(res, err);
559
+ });
560
+ },
561
+ );
562
+
563
+ // Add finding to audit
564
+ app.post(
565
+ '/api/audits/:auditId/findings',
566
+ acl.hasPermission('audits:update'),
567
+ async function (req, res) {
568
+ var settings = await Settings.getAll();
569
+ var audit = await Audit.getAudit(
570
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
571
+ req.params.auditId,
572
+ req.decodedToken.id,
573
+ );
574
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
575
+ Response.Forbidden(
576
+ res,
577
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
578
+ );
579
+ return;
580
+ }
581
+ if (!req.body.title) {
582
+ Response.BadParameters(res, 'Missing some required parameters: title');
583
+ return;
584
+ }
585
+
586
+ var finding = {};
587
+ // Required parameters
588
+ finding.title = req.body.title;
589
+
590
+ // Optional parameters
591
+ if (req.body.vulnType) finding.vulnType = req.body.vulnType;
592
+ if (req.body.description) finding.description = req.body.description;
593
+ if (req.body.observation) finding.observation = req.body.observation;
594
+ if (req.body.remediation) finding.remediation = req.body.remediation;
595
+ if (req.body.remediationComplexity)
596
+ finding.remediationComplexity = req.body.remediationComplexity;
597
+ if (req.body.priority) finding.priority = req.body.priority;
598
+ if (req.body.references) finding.references = req.body.references;
599
+ if (req.body.cwes) finding.cwes = req.body.cwes;
600
+ if (req.body.cvssv3) finding.cvssv3 = req.body.cvssv3;
601
+ if (req.body.poc) finding.poc = req.body.poc;
602
+ if (req.body.scope) finding.scope = req.body.scope;
603
+ if (req.body.status !== undefined) finding.status = req.body.status;
604
+ if (req.body.category) finding.category = req.body.category;
605
+ if (req.body.customFields) finding.customFields = req.body.customFields;
606
+
607
+ if (
608
+ settings.reviews.enabled &&
609
+ settings.reviews.private.removeApprovalsUponUpdate
610
+ ) {
611
+ Audit.updateGeneral(
612
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
613
+ req.params.auditId,
614
+ req.decodedToken.id,
615
+ { approvals: [] },
616
+ );
617
+ }
618
+
619
+ Audit.createFinding(
620
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
621
+ req.params.auditId,
622
+ req.decodedToken.id,
623
+ finding,
624
+ )
625
+ .then(msg => {
626
+ io.to(req.params.auditId).emit('updateAudit');
627
+ Response.Ok(res, msg);
628
+ })
629
+ .catch(err => Response.Internal(res, err));
630
+ },
631
+ );
632
+
633
+ // Get finding of audit
634
+ app.get(
635
+ '/api/audits/:auditId/findings/:findingId',
636
+ acl.hasPermission('audits:read'),
637
+ function (req, res) {
638
+ Audit.getFinding(
639
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
640
+ req.params.auditId,
641
+ req.decodedToken.id,
642
+ req.params.findingId,
643
+ )
644
+ .then(msg => Response.Ok(res, msg))
645
+ .catch(err => Response.Internal(res, err));
646
+ },
647
+ );
648
+
649
+ // Update finding of audit
650
+ app.put(
651
+ '/api/audits/:auditId/findings/:findingId',
652
+ acl.hasPermission('audits:update'),
653
+ async function (req, res) {
654
+ var settings = await Settings.getAll();
655
+ var audit = await Audit.getAudit(
656
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
657
+ req.params.auditId,
658
+ req.decodedToken.id,
659
+ );
660
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
661
+ Response.Forbidden(
662
+ res,
663
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
664
+ );
665
+ return;
666
+ }
667
+
668
+ var finding = {};
669
+ // Optional parameters
670
+ if (req.body.title) finding.title = req.body.title;
671
+ if (req.body.vulnType) finding.vulnType = req.body.vulnType;
672
+ if (!_.isNil(req.body.description))
673
+ finding.description = req.body.description;
674
+ if (!_.isNil(req.body.observation))
675
+ finding.observation = req.body.observation;
676
+ if (!_.isNil(req.body.remediation))
677
+ finding.remediation = req.body.remediation;
678
+ if (req.body.remediationComplexity)
679
+ finding.remediationComplexity = req.body.remediationComplexity;
680
+ if (req.body.priority) finding.priority = req.body.priority;
681
+ if (req.body.references) finding.references = req.body.references;
682
+ if (req.body.cwes) finding.cwes = req.body.cwes;
683
+ if (req.body.cvssv3) finding.cvssv3 = req.body.cvssv3;
684
+ if (!_.isNil(req.body.poc)) finding.poc = req.body.poc;
685
+ if (!_.isNil(req.body.scope)) finding.scope = req.body.scope;
686
+ if (req.body.status !== undefined) finding.status = req.body.status;
687
+ if (req.body.category) finding.category = req.body.category;
688
+ if (req.body.customFields) finding.customFields = req.body.customFields;
689
+ if (req.body.retestDescription)
690
+ finding.retestDescription = req.body.retestDescription;
691
+ if (req.body.retestStatus) finding.retestStatus = req.body.retestStatus;
692
+
693
+ if (
694
+ settings.reviews.enabled &&
695
+ settings.reviews.private.removeApprovalsUponUpdate
696
+ ) {
697
+ Audit.updateGeneral(
698
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
699
+ req.params.auditId,
700
+ req.decodedToken.id,
701
+ { approvals: [] },
702
+ );
703
+ }
704
+
705
+ Audit.updateFinding(
706
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
707
+ req.params.auditId,
708
+ req.decodedToken.id,
709
+ req.params.findingId,
710
+ finding,
711
+ )
712
+ .then(msg => {
713
+ io.to(req.params.auditId).emit('updateAudit');
714
+ Response.Ok(res, msg);
715
+ })
716
+ .catch(err => Response.Internal(res, err));
717
+ },
718
+ );
719
+
720
+ // Delete finding of audit
721
+ app.delete(
722
+ '/api/audits/:auditId/findings/:findingId',
723
+ acl.hasPermission('audits:update'),
724
+ async function (req, res) {
725
+ var settings = await Settings.getAll();
726
+ var audit = await Audit.getAudit(
727
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
728
+ req.params.auditId,
729
+ req.decodedToken.id,
730
+ );
731
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
732
+ Response.Forbidden(
733
+ res,
734
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
735
+ );
736
+ return;
737
+ }
738
+ Audit.deleteFinding(
739
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
740
+ req.params.auditId,
741
+ req.decodedToken.id,
742
+ req.params.findingId,
743
+ )
744
+ .then(msg => {
745
+ io.to(req.params.auditId).emit('updateAudit');
746
+ Response.Ok(res, msg);
747
+ })
748
+ .catch(err => Response.Internal(res, err));
749
+ },
750
+ );
751
+
752
+ // Get section of audit
753
+ app.get(
754
+ '/api/audits/:auditId/sections/:sectionId',
755
+ acl.hasPermission('audits:read'),
756
+ function (req, res) {
757
+ Audit.getSection(
758
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
759
+ req.params.auditId,
760
+ req.decodedToken.id,
761
+ req.params.sectionId,
762
+ )
763
+ .then(msg => Response.Ok(res, msg))
764
+ .catch(err => Response.Internal(res, err));
765
+ },
766
+ );
767
+
768
+ // Update section of audit
769
+ app.put(
770
+ '/api/audits/:auditId/sections/:sectionId',
771
+ acl.hasPermission('audits:update'),
772
+ async function (req, res) {
773
+ var settings = await Settings.getAll();
774
+ var audit = await Audit.getAudit(
775
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
776
+ req.params.auditId,
777
+ req.decodedToken.id,
778
+ );
779
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
780
+ Response.Forbidden(
781
+ res,
782
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
783
+ );
784
+ return;
785
+ }
786
+ if (typeof req.body.customFields === 'undefined') {
787
+ Response.BadParameters(
788
+ res,
789
+ 'Missing some required parameters: customFields',
790
+ );
791
+ return;
792
+ }
793
+ var section = {};
794
+ // Mandatory parameters
795
+ section.customFields = req.body.customFields;
796
+
797
+ // For retrocompatibility with old section.text usage
798
+ if (req.body.text) section.text = req.body.text;
799
+
800
+ if (
801
+ settings.reviews.enabled &&
802
+ settings.reviews.private.removeApprovalsUponUpdate
803
+ ) {
804
+ Audit.updateGeneral(
805
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
806
+ req.params.auditId,
807
+ req.decodedToken.id,
808
+ { approvals: [] },
809
+ );
810
+ }
811
+
812
+ Audit.updateSection(
813
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
814
+ req.params.auditId,
815
+ req.decodedToken.id,
816
+ req.params.sectionId,
817
+ section,
818
+ )
819
+ .then(msg => {
820
+ Response.Ok(res, msg);
821
+ })
822
+ .catch(err => Response.Internal(res, err));
823
+ },
824
+ );
825
+
826
+ // Generate Report for specific audit
827
+ app.get(
828
+ '/api/audits/:auditId/generate',
829
+ acl.hasPermission('audits:read'),
830
+ function (req, res) {
831
+ Audit.getAudit(
832
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
833
+ req.params.auditId,
834
+ req.decodedToken.id,
835
+ )
836
+ .then(async audit => {
837
+ var settings = await Settings.getAll();
838
+
839
+ if (
840
+ settings.reviews.enabled &&
841
+ settings.reviews.public.mandatoryReview &&
842
+ audit.state !== 'APPROVED'
843
+ ) {
844
+ Response.Forbidden(
845
+ res,
846
+ 'Audit was not approved therefore cannot be exported.',
847
+ );
848
+ return;
849
+ }
850
+
851
+ if (!audit.template)
852
+ throw { fn: 'BadParameters', message: 'Template not defined' };
853
+
854
+ var reportDoc = await reportGenerator.generateDoc(audit);
855
+ Response.SendFile(
856
+ res,
857
+ `${audit.name.replace(/[\\\/:*?"<>|]/g, '')}.${audit.template.ext || 'docx'}`,
858
+ reportDoc,
859
+ );
860
+ })
861
+ .catch(err => {
862
+ if (err.code === 'ENOENT')
863
+ Response.BadParameters(res, 'Template File not found');
864
+ else Response.Internal(res, err);
865
+ });
866
+ },
867
+ );
868
+
869
+ // Update sort options of an audit
870
+ app.put(
871
+ '/api/audits/:auditId/sortfindings',
872
+ acl.hasPermission('audits:update'),
873
+ async function (req, res) {
874
+ var settings = await Settings.getAll();
875
+ var audit = await Audit.getAudit(
876
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
877
+ req.params.auditId,
878
+ req.decodedToken.id,
879
+ );
880
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
881
+ Response.Forbidden(
882
+ res,
883
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
884
+ );
885
+ return;
886
+ }
887
+ var update = {};
888
+ // Optional parameters
889
+ if (req.body.sortFindings) update.sortFindings = req.body.sortFindings;
890
+ if (
891
+ settings.reviews.enabled &&
892
+ settings.reviews.private.removeApprovalsUponUpdate
893
+ )
894
+ update.approvals = [];
895
+
896
+ Audit.updateSortFindings(
897
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
898
+ req.params.auditId,
899
+ req.decodedToken.id,
900
+ update,
901
+ )
902
+ .then(msg => {
903
+ io.to(req.params.auditId).emit('updateAudit');
904
+ Response.Ok(res, msg);
905
+ })
906
+ .catch(err => Response.Internal(res, err));
907
+ },
908
+ );
909
+
910
+ // Update finding position (oldIndex -> newIndex)
911
+ app.put(
912
+ '/api/audits/:auditId/movefinding',
913
+ acl.hasPermission('audits:update'),
914
+ async function (req, res) {
915
+ var settings = await Settings.getAll();
916
+ var audit = await Audit.getAudit(
917
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
918
+ req.params.auditId,
919
+ req.decodedToken.id,
920
+ );
921
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
922
+ Response.Forbidden(
923
+ res,
924
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
925
+ );
926
+ return;
927
+ }
928
+ if (
929
+ typeof req.body.oldIndex === 'undefined' ||
930
+ typeof req.body.newIndex === 'undefined'
931
+ ) {
932
+ Response.BadParameters(
933
+ res,
934
+ 'Missing some required parameters: oldIndex, newIndex',
935
+ );
936
+ return;
937
+ }
938
+
939
+ var move = {};
940
+ // Required parameters
941
+ move.oldIndex = req.body.oldIndex;
942
+ move.newIndex = req.body.newIndex;
943
+
944
+ if (
945
+ settings.reviews.enabled &&
946
+ settings.reviews.private.removeApprovalsUponUpdate
947
+ ) {
948
+ Audit.updateGeneral(
949
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
950
+ req.params.auditId,
951
+ req.decodedToken.id,
952
+ { approvals: [] },
953
+ );
954
+ }
955
+
956
+ Audit.moveFindingPosition(
957
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
958
+ req.params.auditId,
959
+ req.decodedToken.id,
960
+ move,
961
+ )
962
+ .then(msg => {
963
+ io.to(req.params.auditId).emit('updateAudit');
964
+ Response.Ok(res, msg);
965
+ })
966
+ .catch(err => Response.Internal(res, err));
967
+ },
968
+ );
969
+
970
+ // Give or remove a reviewer's approval to an audit
971
+ app.put(
972
+ '/api/audits/:auditId/toggleApproval',
973
+ acl.hasPermission('audits:review'),
974
+ async function (req, res) {
975
+ const settings = await Settings.getAll();
976
+
977
+ if (!settings.reviews.enabled) {
978
+ Response.Forbidden(res, 'Audit reviews are not enabled.');
979
+ return;
980
+ }
981
+
982
+ Audit.findById(req.params.auditId)
983
+ .then(audit => {
984
+ if (audit.state !== 'REVIEW' && audit.state !== 'APPROVED') {
985
+ Response.Forbidden(
986
+ res,
987
+ 'The audit is not approvable in the current state.',
988
+ );
989
+ return;
990
+ }
991
+
992
+ var hasApprovedBefore = false;
993
+ var newApprovalsArray = [];
994
+ if (audit.approvals) {
995
+ audit.approvals.forEach(approval => {
996
+ if (approval._id.toString() === req.decodedToken.id) {
997
+ hasApprovedBefore = true;
998
+ } else {
999
+ newApprovalsArray.push(approval);
1000
+ }
1001
+ });
1002
+ }
1003
+
1004
+ if (!hasApprovedBefore) {
1005
+ newApprovalsArray.push({
1006
+ _id: req.decodedToken.id,
1007
+ role: req.decodedToken.role,
1008
+ username: req.decodedToken.username,
1009
+ firstname: req.decodedToken.firstname,
1010
+ lastname: req.decodedToken.lastname,
1011
+ });
1012
+ }
1013
+
1014
+ var update = { approvals: newApprovalsArray };
1015
+ Audit.updateApprovals(
1016
+ acl.isAllowed(req.decodedToken.role, 'audits:review-all'),
1017
+ req.params.auditId,
1018
+ req.decodedToken.id,
1019
+ update,
1020
+ )
1021
+ .then(() => {
1022
+ io.to(req.params.auditId).emit('updateAudit');
1023
+ Response.Ok(res, 'Approval updated successfully.');
1024
+ })
1025
+ .catch(err => {
1026
+ Response.Internal(res, err);
1027
+ });
1028
+ })
1029
+ .catch(err => {
1030
+ Response.Internal(res, err);
1031
+ });
1032
+ },
1033
+ );
1034
+
1035
+ // Sets the audit state to EDIT or REVIEW
1036
+ app.put(
1037
+ '/api/audits/:auditId/updateReadyForReview',
1038
+ acl.hasPermission('audits:update'),
1039
+ async function (req, res) {
1040
+ const settings = await Settings.getAll();
1041
+
1042
+ if (!settings.reviews.enabled) {
1043
+ Response.Forbidden(res, 'Audit reviews are not enabled.');
1044
+ return;
1045
+ }
1046
+
1047
+ var update = {};
1048
+ var audit = await Audit.getAudit(
1049
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
1050
+ req.params.auditId,
1051
+ req.decodedToken.id,
1052
+ );
1053
+
1054
+ if (audit.state !== 'EDIT' && audit.state !== 'REVIEW') {
1055
+ Response.Forbidden(
1056
+ res,
1057
+ 'The audit is not in the proper state for this action.',
1058
+ );
1059
+ return;
1060
+ }
1061
+
1062
+ if (
1063
+ req.body.state != undefined &&
1064
+ (req.body.state === 'EDIT' || req.body.state === 'REVIEW')
1065
+ )
1066
+ update.state = req.body.state;
1067
+
1068
+ if (update.state === 'EDIT') {
1069
+ var newApprovalsArray = [];
1070
+ if (audit.approvals) {
1071
+ audit.approvals.forEach(approval => {
1072
+ if (approval._id.toString() !== req.decodedToken.id) {
1073
+ newApprovalsArray.push(approval);
1074
+ }
1075
+ });
1076
+ update.approvals = newApprovalsArray;
1077
+ }
1078
+ }
1079
+
1080
+ Audit.updateGeneral(
1081
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
1082
+ req.params.auditId,
1083
+ req.decodedToken.id,
1084
+ update,
1085
+ )
1086
+ .then(msg => {
1087
+ io.to(req.params.auditId).emit('updateAudit');
1088
+ Response.Ok(res, msg);
1089
+ })
1090
+ .catch(err => Response.Internal(res, err));
1091
+ },
1092
+ );
1093
+
1094
+ // Update parentId of Audit
1095
+ app.put(
1096
+ '/api/audits/:auditId/updateParent',
1097
+ acl.hasPermission('audits:create'),
1098
+ async function (req, res) {
1099
+ var settings = await Settings.getAll();
1100
+ var audit = await Audit.getAudit(
1101
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
1102
+ req.body.parentId,
1103
+ req.decodedToken.id,
1104
+ );
1105
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
1106
+ Response.Forbidden(
1107
+ res,
1108
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
1109
+ );
1110
+ return;
1111
+ }
1112
+ if (!req.body.parentId) {
1113
+ Response.BadParameters(
1114
+ res,
1115
+ 'Missing some required parameters: parentId',
1116
+ );
1117
+ return;
1118
+ }
1119
+ Audit.updateParent(
1120
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
1121
+ req.params.auditId,
1122
+ req.decodedToken.id,
1123
+ req.body.parentId,
1124
+ )
1125
+ .then(msg => {
1126
+ io.to(req.body.parentId).emit('updateAudit');
1127
+ Response.Ok(res, msg);
1128
+ })
1129
+ .catch(err => Response.Internal(res, err));
1130
+ },
1131
+ );
1132
+
1133
+ // Delete parentId of Audit
1134
+ app.delete(
1135
+ '/api/audits/:auditId/deleteParent',
1136
+ acl.hasPermission('audits:delete'),
1137
+ async function (req, res) {
1138
+ var settings = await Settings.getAll();
1139
+ var audit = await Audit.getAudit(
1140
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
1141
+ req.params.auditId,
1142
+ req.decodedToken.id,
1143
+ );
1144
+ var parentAudit = await Audit.getAudit(
1145
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
1146
+ audit.parentId,
1147
+ req.decodedToken.id,
1148
+ );
1149
+ if (settings.reviews.enabled && parentAudit.state !== 'EDIT') {
1150
+ Response.Forbidden(
1151
+ res,
1152
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
1153
+ );
1154
+ return;
1155
+ }
1156
+ Audit.deleteParent(
1157
+ acl.isAllowed(req.decodedToken.role, 'audits:delete-all'),
1158
+ req.params.auditId,
1159
+ req.decodedToken.id,
1160
+ )
1161
+ .then(msg => {
1162
+ if (msg.parentId) io.to(msg.parentId.toString()).emit('updateAudit');
1163
+ Response.Ok(res, msg);
1164
+ })
1165
+ .catch(err => Response.Internal(res, err));
1166
+ },
1167
+ );
1168
+ };
backend/src/routes/check-cwe-update.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = function (app) {
2
+ const Response = require('../lib/httpResponse.js');
3
+ const acl = require('../lib/auth').acl;
4
+ const networkError = new Error(
5
+ 'Error checking CWE model update: Network response was not ok',
6
+ );
7
+ const timeoutError = new Error(
8
+ 'Error checking CWE mode update: Request timed out',
9
+ );
10
+ const cweConfig = require('../config/config-cwe.json')['cwe-container'];
11
+ const TIMEOUT_MS = cweConfig.check_timeout_ms || 30000;
12
+
13
+ app.get(
14
+ '/api/check-cwe-update',
15
+ acl.hasPermission('check-update:all'),
16
+ async function (req, res) {
17
+ const controller = new AbortController();
18
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
19
+
20
+ try {
21
+ //TODO: Change workaround to a proper solution for self-signed certificates
22
+ if (!cweConfig.host || !cweConfig.port) {
23
+ return Response.BadRequest(
24
+ res,
25
+ new Error('Configuración del servicio incompleta'),
26
+ );
27
+ }
28
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
29
+ const response = await fetch(
30
+ `https://${cweConfig.host}:${cweConfig.port}/${cweConfig.endpoints.check_update_endpoint}`,
31
+ {
32
+ method: 'GET',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ signal: controller.signal,
35
+ },
36
+ );
37
+ clearTimeout(timeout);
38
+
39
+ if (!response.ok) {
40
+ const errorBody = await response.text();
41
+ throw new Error(
42
+ `Error del servidor CWE (${response.status}): ${errorBody}`,
43
+ );
44
+ }
45
+
46
+ const data = await response.json();
47
+ res.json(data);
48
+ } catch (error) {
49
+ console.error('Error en check-cwe-update:', {
50
+ name: error.name,
51
+ message: error.message,
52
+ stack: error.stack,
53
+ });
54
+ error.name === 'AbortError'
55
+ ? Response.Internal(res, { ...timeoutError, details: error.message })
56
+ : Response.Internal(res, { ...networkError, details: error.message });
57
+ }
58
+ },
59
+ );
60
+ };
backend/src/routes/client.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = function (app) {
2
+ var Response = require('../lib/httpResponse.js');
3
+ var Client = require('mongoose').model('Client');
4
+ var acl = require('../lib/auth').acl;
5
+
6
+ // Get clients list
7
+ app.get(
8
+ '/api/clients',
9
+ acl.hasPermission('clients:read'),
10
+ function (req, res) {
11
+ Client.getAll()
12
+ .then(msg => Response.Ok(res, msg))
13
+ .catch(err => Response.Internal(res, err));
14
+ },
15
+ );
16
+
17
+ // Create client
18
+ app.post(
19
+ '/api/clients',
20
+ acl.hasPermission('clients:create'),
21
+ function (req, res) {
22
+ if (!req.body.email) {
23
+ Response.BadParameters(res, 'Required parameters: email');
24
+ return;
25
+ }
26
+
27
+ var client = {};
28
+ // Required parameters
29
+ client.email = req.body.email;
30
+
31
+ // Optional parameters
32
+ if (req.body.lastname) client.lastname = req.body.lastname;
33
+ if (req.body.firstname) client.firstname = req.body.firstname;
34
+ if (req.body.phone) client.phone = req.body.phone;
35
+ if (req.body.cell) client.cell = req.body.cell;
36
+ if (req.body.title) client.title = req.body.title;
37
+ var company = null;
38
+ if (req.body.company && req.body.company.name)
39
+ company = req.body.company.name;
40
+
41
+ Client.create(client, company)
42
+ .then(msg => Response.Created(res, msg))
43
+ .catch(err => Response.Internal(res, err));
44
+ },
45
+ );
46
+
47
+ // Update client
48
+ app.put(
49
+ '/api/clients/:id',
50
+ acl.hasPermission('clients:update'),
51
+ function (req, res) {
52
+ var client = {};
53
+ // Optional parameters
54
+ if (req.body.email) client.email = req.body.email;
55
+ client.lastname = req.body.lastname || null;
56
+ client.firstname = req.body.firstname || null;
57
+ client.phone = req.body.phone || null;
58
+ client.cell = req.body.cell || null;
59
+ client.title = req.body.title || null;
60
+ var company = null;
61
+ if (req.body.company && req.body.company.name)
62
+ company = req.body.company.name;
63
+
64
+ Client.update(req.params.id, client, company)
65
+ .then(msg => Response.Ok(res, 'Client updated successfully'))
66
+ .catch(err => Response.Internal(res, err));
67
+ },
68
+ );
69
+
70
+ // Delete client
71
+ app.delete(
72
+ '/api/clients/:id',
73
+ acl.hasPermission('clients:delete'),
74
+ function (req, res) {
75
+ Client.delete(req.params.id)
76
+ .then(msg => Response.Ok(res, 'Client deleted successfully'))
77
+ .catch(err => Response.Internal(res, err));
78
+ },
79
+ );
80
+ };