Spaces:
Runtime error
Runtime error
initialize project structure with essential configurations and components
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .deepsource.toml +25 -0
- .env.example +2 -0
- .gitattributes +1 -0
- .gitconfig +2 -0
- .gitignore +30 -0
- LICENSE.md +22 -0
- app.py +7 -0
- backend/.dockerignore +3 -0
- backend/.prettierrc +12 -0
- backend/Dockerfile +12 -0
- backend/Dockerfile.dev +11 -0
- backend/Dockerfile.test +12 -0
- backend/README.md +21 -0
- backend/babel.config.js +12 -0
- backend/docker-compose.dev.yml +41 -0
- backend/docker-compose.test.yml +30 -0
- backend/jest.config.js +4 -0
- backend/report-templates/.gitignore +1 -0
- backend/report-templates/.gitkeep +0 -0
- backend/src/app.js +170 -0
- backend/src/config/config-cwe.json +12 -0
- backend/src/config/config.json +33 -0
- backend/src/config/roles.json +6 -0
- backend/src/lib/auth.js +193 -0
- backend/src/lib/custom-generator.js +94 -0
- backend/src/lib/cvsscalc31.js +1091 -0
- backend/src/lib/html2ooxml.js +204 -0
- backend/src/lib/httpResponse.js +82 -0
- backend/src/lib/passwordpolicy.js +7 -0
- backend/src/lib/report-filters.js +423 -0
- backend/src/lib/report-generator.js +707 -0
- backend/src/lib/utils.js +58 -0
- backend/src/models/audit-type.js +114 -0
- backend/src/models/audit.js +1195 -0
- backend/src/models/client.js +128 -0
- backend/src/models/company.js +95 -0
- backend/src/models/custom-field.js +147 -0
- backend/src/models/custom-section.js +105 -0
- backend/src/models/image.js +78 -0
- backend/src/models/language.js +84 -0
- backend/src/models/settings.js +188 -0
- backend/src/models/template.js +110 -0
- backend/src/models/user.js +430 -0
- backend/src/models/vulnerability-category.js +124 -0
- backend/src/models/vulnerability-type.js +96 -0
- backend/src/models/vulnerability-update.js +190 -0
- backend/src/models/vulnerability.js +272 -0
- backend/src/routes/audit.js +1168 -0
- backend/src/routes/check-cwe-update.js +60 -0
- 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 = { '<': '<', '>': '>', '&': '&' };
|
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 |
+
};
|