diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2645e9e38579fa27dbb608f92bfad81d8c315d89
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,25 @@
+version = 1
+xclude_patterns = [
+ "dist/**",
+ "**/node_modules/",
+ "js/**/*.min.js",
+ "backend/**/*"
+]
+
+[[analyzers]]
+name = "javascript"
+enabled = true
+
+[analyzers.meta]
+environment = ["mongo"]
+plugins = ["react"]
+skip_doc_coverage = ["class-expression", "method-definition"]
+dependency_file_paths = [
+ "./frontend/",
+ "./"
+]
+dialect = "typescript"
+
+[[transformers]]
+name = "prettier"
+enabled = false
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..b7c10208db6fc4021895021b9715e6569c4228ca
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+CWE_MODEL_URL=https://drive.usercontent.google.com/download?id=1OtRNObv-Il2B5nDnpzMSGj_yBJAlskuS&export=download&confirm=```
+CVSS_MODEL_URL=https://drive.usercontent.google.com/download?id=1nS1lQpVVJ431wUyVSs5_Srega6QVPyc8&export=download&confirm=
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..d22cef17abd65c6b9edcc26dfb84d6ae5fe6c6ac 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitconfig b/.gitconfig
new file mode 100644
index 0000000000000000000000000000000000000000..41e358c680b809600e1d5465f63ee78e2d63e48f
--- /dev/null
+++ b/.gitconfig
@@ -0,0 +1,2 @@
+[pull]
+ rebase = yes
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..91e56eca994d1208c6a2c105f4bbbcdc02ccd053
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.DS_Store
+.thumbs.db
+node_modules
+/dist
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+mongo-data*
+.quasar
+.venv
+package-lock.json
+.env
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+.dccache
+.sourcery.yaml
+
+# Version managers
+.tool-versions
+
+
+# modelo
+cwe_api/modelo_cwe
+cwe_api/utils
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..1385696fe3c142faed20d82952680ba521c49878
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2024 Camilo Vera Vidales
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..2dc290952d99f36fd7d09cdd4a1a1e37c1a42914
--- /dev/null
+++ b/app.py
@@ -0,0 +1,7 @@
+from fastapi import FastAPI
+
+app = FastAPI()
+
+@app.get("/")
+def greet_json():
+ return {"Hello": "World!"}
\ No newline at end of file
diff --git a/backend/.dockerignore b/backend/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..a2df488f2ad96ca8db291fdc4cd954e6ecf2f34f
--- /dev/null
+++ b/backend/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+mongo-data
+mongo-data-dev
diff --git a/backend/.prettierrc b/backend/.prettierrc
new file mode 100644
index 0000000000000000000000000000000000000000..83c0c9f23f4fbf2f5ef66ee0dfed5d4e24d581ab
--- /dev/null
+++ b/backend/.prettierrc
@@ -0,0 +1,12 @@
+{
+ "arrowParens": "avoid",
+ "singleQuote": true,
+ "overrides": [
+ {
+ "files": ".changeset/**/*",
+ "options": {
+ "singleQuote": false
+ }
+ }
+ ]
+}
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..f9d3e6c9f4f7a17bf734d1b2784fb20591094455
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:lts-alpine
+
+RUN mkdir -p /app
+WORKDIR /app
+COPY package*.json ./
+RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
+RUN npm install
+COPY . .
+EXPOSE 4242
+ENV NODE_ENV prod
+ENV NODE_ICU_DATA=node_modules/full-icu
+ENTRYPOINT ["npm", "start"]
diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev
new file mode 100644
index 0000000000000000000000000000000000000000..af9fad4f7ac51ab19f865a59439cea741dbeeac9
--- /dev/null
+++ b/backend/Dockerfile.dev
@@ -0,0 +1,11 @@
+FROM node:lts-alpine
+
+RUN mkdir -p /app
+WORKDIR /app
+COPY package*.json ./
+RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
+RUN npm install
+COPY . .
+ENV NODE_ENV dev
+ENV NODE_ICU_DATA=node_modules/full-icu
+ENTRYPOINT [ "npm", "run", "dev"]
\ No newline at end of file
diff --git a/backend/Dockerfile.test b/backend/Dockerfile.test
new file mode 100644
index 0000000000000000000000000000000000000000..5cc4146dc02ecae7b038ff2b0cec864b84bae889
--- /dev/null
+++ b/backend/Dockerfile.test
@@ -0,0 +1,12 @@
+FROM node:lts-alpine
+
+RUN mkdir -p /app
+WORKDIR /app
+COPY package*.json ./
+RUN apk --no-cache add --virtual builds-deps build-base python3 git libreoffice ttf-liberation
+RUN npm install
+COPY . .
+ENV NODE_ENV test
+ENV NODE_ICU_DATA=node_modules/full-icu
+RUN npm install
+ENTRYPOINT ["npm", "run", "test"]
\ No newline at end of file
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2f98d45ede1d3bead0f522227e956b4611293dc8
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,21 @@
+# Installation for developpment environnment
+
+*Source code can be modified live and application will automatically reload on changes.*
+
+Build and run Docker containers
+```
+docker-compose -f ./docker-compose.dev.yml up -d --build
+```
+
+Display container logs
+```
+docker-compose logs -f
+```
+
+Stop/Start container
+```
+docker-compose stop
+docker-compose start
+```
+
+API is accessible through https://localhost:5252/api
\ No newline at end of file
diff --git a/backend/babel.config.js b/backend/babel.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..f037a1a2a1477424dfca78799d57f863a5b74f81
--- /dev/null
+++ b/backend/babel.config.js
@@ -0,0 +1,12 @@
+module.exports = {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: {
+ node: 'current',
+ },
+ },
+ ],
+ ],
+};
diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml
new file mode 100644
index 0000000000000000000000000000000000000000..279ac2f311ec0bde2f759b484a6e8bde65b46db2
--- /dev/null
+++ b/backend/docker-compose.dev.yml
@@ -0,0 +1,41 @@
+version: '3'
+services:
+ mongodb:
+ image: mongo:4.2.15
+ container_name: mongo-auditforge-dev
+ volumes:
+ - ./mongo-data-dev:/data/db
+ restart: always
+ ports:
+ - 127.0.0.1:27017:27017
+ environment:
+ - MONGO_DB:auditforge
+ networks:
+ - backend
+
+ auditforge-backend:
+ build:
+ context: .
+ dockerfile: Dockerfile.dev
+ image: auditforge:backend-dev
+ container_name: auditforge-backend-dev
+ volumes:
+ - ./src:/app/src
+ - ./ssl:/app/ssl
+ - ./report-templates:/app/report-templates
+ depends_on:
+ - mongodb
+ restart: always
+ ports:
+ - 5252:5252
+ links:
+ - mongodb
+ networks:
+ - backend
+
+volumes:
+ mongo-data-dev:
+
+networks:
+ backend:
+ driver: bridge
diff --git a/backend/docker-compose.test.yml b/backend/docker-compose.test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ddc022bc991b94d4ef609c1e6375e2a383128bf
--- /dev/null
+++ b/backend/docker-compose.test.yml
@@ -0,0 +1,30 @@
+version: '3'
+services:
+ mongodb-test:
+ image: mongo:4.2.15
+ container_name: mongo-auditforge-test
+ volumes:
+ - ./mongo-data-test:/data/db
+ restart: always
+ environment:
+ - MONGO_DB:auditforge
+ network_mode: host
+
+ backend-test:
+ image: auditforge:backend-test
+ build:
+ context: .
+ dockerfile: Dockerfile.test
+ container_name: auditforge-backend-test
+ volumes:
+ - ./tests:/app/tests
+ - ./src:/app/src
+ - ./jest.config.js:/app/jest.config.js
+ environment:
+ API_URL: https://localhost:4242
+ depends_on:
+ - mongodb-test
+ network_mode: host
+
+volumes:
+ mongo-data-test:
diff --git a/backend/jest.config.js b/backend/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..3913dcf54177bda8714a17a25c25b3e92bea40ff
--- /dev/null
+++ b/backend/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ testEnvironment: 'node',
+ verbose: true,
+};
diff --git a/backend/report-templates/.gitignore b/backend/report-templates/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ebebac81e6f4f7460d211346eef6b03dede8f3ac
--- /dev/null
+++ b/backend/report-templates/.gitignore
@@ -0,0 +1 @@
+*.docx
\ No newline at end of file
diff --git a/backend/report-templates/.gitkeep b/backend/report-templates/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/backend/src/app.js b/backend/src/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..527e03eb774a1c0438dcd6b6aa41310ae6be77f9
--- /dev/null
+++ b/backend/src/app.js
@@ -0,0 +1,170 @@
+var fs = require('fs');
+var app = require('express')();
+
+var https = require('https').Server(
+ {
+ key: fs.readFileSync(__dirname + '/../ssl/server.key'),
+ cert: fs.readFileSync(__dirname + '/../ssl/server.cert'),
+
+ // TLS Versions
+ maxVersion: 'TLSv1.3',
+ minVersion: 'TLSv1.2',
+
+ // Hardened configuration
+ ciphers:
+ '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',
+
+ honorCipherOrder: false,
+ },
+ app,
+);
+app.disable('x-powered-by');
+
+var io = require('socket.io')(https, {
+ cors: {
+ origin: '*',
+ },
+});
+var bodyParser = require('body-parser');
+var cookieParser = require('cookie-parser');
+var utils = require('./lib/utils');
+
+// Get configuration
+var env = process.env.NODE_ENV || 'dev';
+var config = require('./config/config.json')[env];
+global.__basedir = __dirname;
+
+// Database connection
+var mongoose = require('mongoose');
+// Use native promises
+mongoose.Promise = global.Promise;
+// Trim all Strings
+mongoose.Schema.Types.String.set('trim', true);
+
+mongoose.connect(
+ `mongodb://${config.database.server}:${config.database.port}/${config.database.name}`,
+ {},
+);
+
+// Models import
+require('./models/user');
+require('./models/audit');
+require('./models/client');
+require('./models/company');
+require('./models/template');
+require('./models/vulnerability');
+require('./models/vulnerability-update');
+require('./models/language');
+require('./models/audit-type');
+require('./models/vulnerability-type');
+require('./models/vulnerability-category');
+require('./models/custom-section');
+require('./models/custom-field');
+require('./models/image');
+require('./models/settings');
+
+// Socket IO configuration
+io.on('connection', socket => {
+ socket.on('join', data => {
+ console.log(
+ `user ${data.username.replace(/\n|\r/g, '')} joined room ${data.room.replace(/\n|\r/g, '')}`,
+ );
+ socket.username = data.username;
+ do {
+ socket.color =
+ '#' + (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
+ } while (socket.color === '#77c84e');
+ socket.join(data.room);
+ io.to(data.room).emit('updateUsers');
+ });
+ socket.on('leave', data => {
+ console.log(
+ `user ${data.username.replace(/\n|\r/g, '')} left room ${data.room.replace(/\n|\r/g, '')}`,
+ );
+ socket.leave(data.room);
+ io.to(data.room).emit('updateUsers');
+ });
+ socket.on('updateUsers', data => {
+ var userList = [
+ ...new Set(
+ utils.getSockets(io, data.room).map(s => {
+ var user = {};
+ user.username = s.username;
+ user.color = s.color;
+ user.menu = s.menu;
+ if (s.finding) user.finding = s.finding;
+ if (s.section) user.section = s.section;
+ return user;
+ }),
+ ),
+ ];
+ io.to(data.room).emit('roomUsers', userList);
+ });
+ socket.on('menu', data => {
+ socket.menu = data.menu;
+ data.finding ? (socket.finding = data.finding) : delete socket.finding;
+ data.section ? (socket.section = data.section) : delete socket.section;
+ io.to(data.room).emit('updateUsers');
+ });
+ socket.on('disconnect', () => {
+ socket.broadcast.emit('updateUsers');
+ });
+});
+
+// CORS
+app.use(function (req, res, next) {
+ res.header('Access-Control-Allow-Origin', req.headers.origin);
+ res.header('Access-Control-Allow-Methods', 'GET,POST,DELETE,PUT,OPTIONS');
+ res.header(
+ 'Access-Control-Allow-Headers',
+ 'Origin, X-Requested-With, Content-Type, Accept',
+ );
+ res.header('Access-Control-Expose-Headers', 'Content-Disposition');
+ res.header('Access-Control-Allow-Credentials', 'true');
+ next();
+});
+
+// CSP
+app.use(function (req, res, next) {
+ res.header(
+ 'Content-Security-Policy',
+ "default-src 'none'; form-action 'none'; base-uri 'self'; frame-ancestors 'none'; sandbox; require-trusted-types-for 'script';",
+ );
+ next();
+});
+
+app.use(bodyParser.json({ limit: '100mb' }));
+app.use(
+ bodyParser.urlencoded({
+ limit: '10mb',
+ extended: false, // do not need to take care about images, videos -> false: only strings
+ }),
+);
+
+app.use(cookieParser());
+
+// Routes import
+require('./routes/user')(app);
+require('./routes/audit')(app, io);
+require('./routes/client')(app);
+require('./routes/company')(app);
+require('./routes/vulnerability')(app);
+require('./routes/template')(app);
+require('./routes/vulnerability')(app);
+require('./routes/data')(app);
+require('./routes/image')(app);
+require('./routes/settings')(app);
+require('./routes/cwe')(app);
+require('./routes/cvss')(app);
+require('./routes/check-cwe-update')(app);
+require('./routes/update-cwe-model')(app);
+
+app.get('*', function (req, res) {
+ res.status(404).json({ status: 'error', data: 'Route undefined' });
+});
+
+// Start server
+
+https.listen(config.port, config.host);
+
+module.exports = app;
diff --git a/backend/src/config/config-cwe.json b/backend/src/config/config-cwe.json
new file mode 100644
index 0000000000000000000000000000000000000000..27bace4992ae6e71c0612dd13b973df94e9356cc
--- /dev/null
+++ b/backend/src/config/config-cwe.json
@@ -0,0 +1,12 @@
+{
+ "cwe-container": {
+ "host": "auditforge-cwe-api",
+ "port": 8000,
+ "check_timeout_ms": 30000,
+ "update_timeout_ms": 120000,
+ "endpoints": {
+ "check_update_endpoint": "check_cwe_update",
+ "update_cwe_endpoint": "update_cwe_model"
+ }
+ }
+}
diff --git a/backend/src/config/config.json b/backend/src/config/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..f03617743bb6a50683d181a3f31c2bcf038c1ea7
--- /dev/null
+++ b/backend/src/config/config.json
@@ -0,0 +1,33 @@
+{
+ "dev": {
+ "port": 5252,
+ "host": "",
+ "database": {
+ "name": "auditforge",
+ "server": "mongo-auditforge-dev",
+ "port": "27017"
+ },
+ "jwtSecret": "1a039133523dfca9cd5d0ac385c16f0f061558a96dc57384854ef90ed53e86dd",
+ "jwtRefreshSecret": "95b0c96984c301d94b41c3d2306fd041a001c58516eb5a23f83044822f42e558"
+ },
+ "prod": {
+ "port": 4242,
+ "host": "",
+ "database": {
+ "name": "auditforge",
+ "server": "mongo-auditforge",
+ "port": "27017"
+ },
+ "jwtSecret": "8565a16daedc531581393a08812af3c9043e702069216c54bd51c6613bcf9811",
+ "jwtRefreshSecret": "54f27d78990b5f4537702dbf97d9d746c7cb8172f070a1212933c877e8fc98a8"
+ },
+ "test": {
+ "port": 6262,
+ "host": "",
+ "database": {
+ "name": "auditforge",
+ "server": "127.0.0.1",
+ "port": "27017"
+ }
+ }
+}
diff --git a/backend/src/config/roles.json b/backend/src/config/roles.json
new file mode 100644
index 0000000000000000000000000000000000000000..56406ab8e2716b3f329e9205352d9444bcbb08a4
--- /dev/null
+++ b/backend/src/config/roles.json
@@ -0,0 +1,6 @@
+{
+ "report": {
+ "inherits": ["user"],
+ "allows": ["audits:read-all"]
+ }
+}
diff --git a/backend/src/lib/auth.js b/backend/src/lib/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9756549feb1b4c0ae3d9bee9004967383a9fc6f
--- /dev/null
+++ b/backend/src/lib/auth.js
@@ -0,0 +1,193 @@
+// Dynamic generation of JWT Secret if not exist (different for each environnment)
+var fs = require('fs');
+var env = process.env.NODE_ENV || 'dev';
+var config = require('../config/config.json');
+
+if (!config[env].jwtSecret) {
+ config[env].jwtSecret = require('crypto').randomBytes(32).toString('hex');
+ var configString = JSON.stringify(config, null, 4);
+ fs.writeFileSync(`${__basedir}/config/config.json`, configString);
+}
+if (!config[env].jwtRefreshSecret) {
+ config[env].jwtRefreshSecret = require('crypto')
+ .randomBytes(32)
+ .toString('hex');
+ var configString = JSON.stringify(config, null, 4);
+ fs.writeFileSync(`${__basedir}/config/config.json`, configString);
+}
+
+var jwtSecret = config[env].jwtSecret;
+exports.jwtSecret = jwtSecret;
+
+var jwtRefreshSecret = config[env].jwtRefreshSecret;
+exports.jwtRefreshSecret = jwtRefreshSecret;
+
+/* ROLES LOGIC
+
+ role_name: {
+ allows: [],
+ inherits: []
+ }
+ allows: allowed permissions to access | use * for all
+ inherits: inherits other users "allows"
+*/
+
+const builtInRoles = {
+ user: {
+ allows: [
+ // Audits
+ 'audits:create',
+ 'audits:read',
+ 'audits:update',
+ 'audits:delete',
+ // Images
+ 'images:create',
+ 'images:read',
+ // Clients
+ 'clients:create',
+ 'clients:read',
+ 'clients:update',
+ 'clients:delete',
+ // Companies
+ 'companies:create',
+ 'companies:read',
+ 'companies:update',
+ 'companies:delete',
+ // Languages
+ 'languages:read',
+ // Audit Types
+ 'audit-types:read',
+ // Vulnerability Types
+ 'vulnerability-types:read',
+ // Vulnerability Categories
+ 'vulnerability-categories:read',
+ // Sections Data
+ 'sections:read',
+ // Templates
+ 'templates:read',
+ // Users
+ 'users:read',
+ // Roles
+ 'roles:read',
+ // Vulnerabilities
+ 'vulnerabilities:read',
+ 'vulnerability-updates:create',
+ // Custom Fields
+ 'custom-fields:read',
+ // Settings
+ 'settings:read-public',
+ // Classify
+ 'classify:all',
+ // Check CWE Update
+ 'check-update:all',
+ // Update CWE Model
+ 'update-model:all',
+ ],
+ },
+ admin: {
+ allows: '*',
+ },
+};
+
+try {
+ var customRoles = require('../config/roles.json');
+} catch (error) {
+ var customRoles = [];
+}
+var roles = { ...customRoles, ...builtInRoles };
+
+class ACL {
+ constructor(roles) {
+ if (typeof roles !== 'object') {
+ throw new TypeError('Expected an object as input');
+ }
+ this.roles = roles;
+ }
+
+ isAllowed(role, permission) {
+ // Check if role exists
+ if (!this.roles[role] && !this.roles['user']) {
+ return false;
+ }
+
+ let $role = this.roles[role] || this.roles['user']; // Default to user role in case of inexistant role
+ // Check if role is allowed with permission
+ if (
+ $role.allows &&
+ ($role.allows === '*' || $role.allows.indexOf(permission) !== -1)
+ ) {
+ return true;
+ }
+
+ // Check if there is inheritance
+ if (!$role.inherits || $role.inherits.length < 1) {
+ return false;
+ }
+
+ // Recursive check childs until true or false
+ return $role.inherits.some(role => this.isAllowed(role, permission));
+ }
+
+ hasPermission(permission) {
+ var Response = require('./httpResponse');
+ var jwt = require('jsonwebtoken');
+
+ return (req, res, next) => {
+ if (!req.cookies['token']) {
+ Response.Unauthorized(res, 'No token provided');
+ return;
+ }
+
+ var cookie = req.cookies['token'].split(' ');
+ if (cookie.length !== 2 || cookie[0] !== 'JWT') {
+ Response.Unauthorized(res, 'Bad token type');
+ return;
+ }
+
+ var token = cookie[1];
+ jwt.verify(token, jwtSecret, (err, decoded) => {
+ if (err) {
+ if (err.name === 'TokenExpiredError')
+ Response.Unauthorized(res, 'Expired token');
+ else Response.Unauthorized(res, 'Invalid token');
+ return;
+ }
+
+ if (
+ permission === 'validtoken' ||
+ this.isAllowed(decoded.role, permission)
+ ) {
+ req.decodedToken = decoded;
+ return next();
+ } else {
+ Response.Forbidden(res, 'Insufficient privileges');
+ return;
+ }
+ });
+ };
+ }
+
+ buildRoles(role) {
+ var currentRole = this.roles[role] || this.roles['user']; // Default to user role in case of inexistant role
+
+ var result = currentRole.allows || [];
+
+ if (currentRole.inherits) {
+ currentRole.inherits.forEach(element => {
+ result = [...new Set([...result, ...this.buildRoles(element)])];
+ });
+ }
+
+ return result;
+ }
+
+ getRoles(role) {
+ var result = this.buildRoles(role);
+
+ if (result.includes('*')) return '*';
+
+ return result;
+ }
+}
+
+exports.acl = new ACL(roles);
diff --git a/backend/src/lib/custom-generator.js b/backend/src/lib/custom-generator.js
new file mode 100644
index 0000000000000000000000000000000000000000..d72865243e5d96d3b9e12afb1a2bcb803493d4c2
--- /dev/null
+++ b/backend/src/lib/custom-generator.js
@@ -0,0 +1,94 @@
+var expressions = require('angular-expressions');
+
+// Apply all customs functions
+function apply(data) {}
+exports.apply = apply;
+
+// *** Custom modifications of audit data for usage in word template
+
+// *** Custome Angular expressions filters ***
+
+var filters = {};
+
+// Convert input CVSS criteria into French: {input | criteriaFR}
+expressions.filters.criteriaFR = function (input) {
+ var result = 'Non défini';
+
+ if (input === 'Network') result = 'Réseau';
+ else if (input === 'Adjacent Network') result = 'Réseau Local';
+ else if (input === 'Local') result = 'Local';
+ else if (input === 'Physical') result = 'Physique';
+ else if (input === 'None') result = 'Aucun';
+ else if (input === 'Low') result = 'Faible';
+ else if (input === 'High') result = 'Haute';
+ else if (input === 'Required') result = 'Requis';
+ else if (input === 'Unchanged') result = 'Inchangé';
+ else if (input === 'Changed') result = 'Changé';
+
+ return result;
+};
+
+// Convert input date with parameter s (full,short): {input | convertDate: 's'}
+expressions.filters.convertDateFR = function (input, s) {
+ var date = new Date(input);
+ if (date !== 'Invalid Date') {
+ var monthsFull = [
+ 'Janvier',
+ 'Février',
+ 'Mars',
+ 'Avril',
+ 'Mai',
+ 'Juin',
+ 'Juillet',
+ 'Août',
+ 'Septembre',
+ 'Octobre',
+ 'Novembre',
+ 'Décembre',
+ ];
+ var monthsShort = [
+ '01',
+ '02',
+ '03',
+ '04',
+ '05',
+ '06',
+ '07',
+ '08',
+ '09',
+ '10',
+ '11',
+ '12',
+ ];
+ var days = [
+ 'Dimanche',
+ 'Lundi',
+ 'Mardi',
+ 'Mercredi',
+ 'Jeudi',
+ 'Vendredi',
+ 'Samedi',
+ ];
+ var day = date.getUTCDate();
+ var month = date.getUTCMonth();
+ var year = date.getUTCFullYear();
+ if (s === 'full') {
+ return (
+ days[date.getUTCDay()] +
+ ' ' +
+ (day < 10 ? '0' + day : day) +
+ ' ' +
+ monthsFull[month] +
+ ' ' +
+ year
+ );
+ }
+ if (s === 'short') {
+ return (
+ (day < 10 ? '0' + day : day) + '/' + monthsShort[month] + '/' + year
+ );
+ }
+ }
+};
+
+exports.expressions = expressions;
diff --git a/backend/src/lib/cvsscalc31.js b/backend/src/lib/cvsscalc31.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d55b51db420873043a9db02da4d76e7b16dece5
--- /dev/null
+++ b/backend/src/lib/cvsscalc31.js
@@ -0,0 +1,1091 @@
+/* Copyright (c) 2019, FIRST.ORG, INC.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+ * following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+ * disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
+ * following disclaimer in the documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/* This JavaScript contains two main functions. Both take CVSS metric values and calculate CVSS scores for Base,
+ * Temporal and Environmental metric groups, their associated severity ratings, and an overall Vector String.
+ *
+ * Use CVSS31.calculateCVSSFromMetrics if you wish to pass metric values as individual parameters.
+ * Use CVSS31.calculateCVSSFromVector if you wish to pass metric values as a single Vector String.
+ *
+ * Changelog
+ *
+ * 2019-06-01 Darius Wiles Updates for CVSS version 3.1:
+ *
+ * 1) The CVSS31.roundUp1 function now performs rounding using integer arithmetic to
+ * eliminate problems caused by tiny errors introduced during JavaScript math
+ * operations. Thanks to Stanislav Kontar of Red Hat for suggesting and testing
+ * various implementations.
+ *
+ * 2) Environmental formulas changed to prevent the Environmental Score decreasing when
+ * the value of an Environmental metric is raised. The problem affected a small
+ * percentage of CVSS v3.0 metrics. The change is to the modifiedImpact
+ * formula, but only affects scores where the Modified Scope is Changed (or the
+ * Scope is Changed if Modified Scope is Not Defined).
+ *
+ * 3) The JavaScript object containing everything in this file has been renamed from
+ * "CVSS" to "CVSS31" to allow both objects to be included without causing a
+ * naming conflict.
+ *
+ * 4) Variable names and code order have changed to more closely reflect the formulas
+ * in the CVSS v3.1 Specification Document.
+ *
+ * 5) A successful call to calculateCVSSFromMetrics now returns sub-formula values.
+ *
+ * Note that some sets of metrics will produce different scores between CVSS v3.0 and
+ * v3.1 as a result of changes 1 and 2. See the explanation of changes between these
+ * two standards in the CVSS v3.1 User Guide for more details.
+ *
+ * 2018-02-15 Darius Wiles Added a missing pair of parentheses in the Environmental score, specifically
+ * in the code setting envScore in the main clause (not the else clause). It was changed
+ * from "min (...), 10" to "min ((...), 10)". This correction does not alter any final
+ * Environmental scores.
+ *
+ * 2015-08-04 Darius Wiles Added CVSS.generateXMLFromMetrics and CVSS.generateXMLFromVector functions to return
+ * XML string representations of: a set of metric values; or a Vector String respectively.
+ * Moved all constants and functions to an object named "CVSS" to
+ * reduce the chance of conflicts in global variables when this file is combined with
+ * other JavaScript code. This will break all existing code that uses this file until
+ * the string "CVSS." is prepended to all references. The "Exploitability" metric has been
+ * renamed "Exploit Code Maturity" in the specification, so the same change has been made
+ * in the code in this file.
+ *
+ * 2015-04-24 Darius Wiles Environmental formula modified to eliminate undesirable behavior caused by subtle
+ * differences in rounding between Temporal and Environmental formulas that often
+ * caused the latter to be 0.1 lower than than the former when all Environmental
+ * metrics are "Not defined". Also added a RoundUp1 function to simplify formulas.
+ *
+ * 2015-04-09 Darius Wiles Added calculateCVSSFromVector function, license information, cleaned up code and improved
+ * comments.
+ *
+ * 2014-12-12 Darius Wiles Initial release for CVSS 3.0 Preview 2.
+ */
+
+// Constants used in the formula. They are not declared as "const" to avoid problems in older browsers.
+
+var CVSS31 = {};
+
+CVSS31.CVSSVersionIdentifier = 'CVSS:3.1';
+CVSS31.exploitabilityCoefficient = 8.22;
+CVSS31.scopeCoefficient = 1.08;
+
+// A regular expression to validate that a CVSS 3.1 vector string is well formed. It checks metrics and metric
+// values. It does not check that a metric is specified more than once and it does not check that all base
+// metrics are present. These checks need to be performed separately.
+
+CVSS31.vectorStringRegex_31 =
+ /^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])$/;
+
+// Associative arrays mapping each metric value to the constant defined in the CVSS scoring formula in the CVSS v3.1
+// specification.
+
+CVSS31.Weight = {
+ AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
+ AC: { H: 0.44, L: 0.77 },
+ PR: {
+ U: { N: 0.85, L: 0.62, H: 0.27 }, // These values are used if Scope is Unchanged
+ C: { N: 0.85, L: 0.68, H: 0.5 },
+ }, // These values are used if Scope is Changed
+ UI: { N: 0.85, R: 0.62 },
+ S: { U: 6.42, C: 7.52 }, // Note: not defined as constants in specification
+ CIA: { N: 0, L: 0.22, H: 0.56 }, // C, I and A have the same weights
+
+ E: { X: 1, U: 0.91, P: 0.94, F: 0.97, H: 1 },
+ RL: { X: 1, O: 0.95, T: 0.96, W: 0.97, U: 1 },
+ RC: { X: 1, U: 0.92, R: 0.96, C: 1 },
+
+ CIAR: { X: 1, L: 0.5, M: 1, H: 1.5 }, // CR, IR and AR have the same weights
+};
+
+// Severity rating bands, as defined in the CVSS v3.1 specification.
+
+CVSS31.severityRatings = [
+ { name: 'None', bottom: 0.0, top: 0.0 },
+ { name: 'Low', bottom: 0.1, top: 3.9 },
+ { name: 'Medium', bottom: 4.0, top: 6.9 },
+ { name: 'High', bottom: 7.0, top: 8.9 },
+ { name: 'Critical', bottom: 9.0, top: 10.0 },
+];
+
+/* ** CVSS31.calculateCVSSFromMetrics **
+ *
+ * Takes Base, Temporal and Environmental metric values as individual parameters. Their values are in the short format
+ * defined in the CVSS v3.1 standard definition of the Vector String. For example, the AttackComplexity parameter
+ * should be either "H" or "L".
+ *
+ * Returns Base, Temporal and Environmental scores, severity ratings, and an overall Vector String. All Base metrics
+ * are required to generate this output. All Temporal and Environmental metric values are optional. Any that are not
+ * passed default to "X" ("Not Defined").
+ *
+ * The output is an object which always has a property named "success".
+ *
+ * If no errors are encountered, success is Boolean "true", and the following other properties are defined containing
+ * scores, severities and a vector string:
+ * baseMetricScore, baseSeverity,
+ * temporalMetricScore, temporalSeverity,
+ * environmentalMetricScore, environmentalSeverity,
+ * vectorString
+ *
+ * The following properties are also defined, and contain sub-formula values:
+ * baseISS, baseImpact, baseExploitability,
+ * environmentalMISS, environmentalModifiedImpact, environmentalModifiedExploitability
+ *
+ *
+ * If errors are encountered, success is Boolean "false", and the following other properties are defined:
+ * errorType - a string indicating the error. Either:
+ * "MissingBaseMetric", if at least one Base metric has not been defined; or
+ * "UnknownMetricValue", if at least one metric value is invalid.
+ * errorMetrics - an array of strings representing the metrics at fault. The strings are abbreviated versions of the
+ * metrics, as defined in the CVSS v3.1 standard definition of the Vector String.
+ */
+CVSS31.calculateCVSSFromMetrics = function (
+ AttackVector,
+ AttackComplexity,
+ PrivilegesRequired,
+ UserInteraction,
+ Scope,
+ Confidentiality,
+ Integrity,
+ Availability,
+ ExploitCodeMaturity,
+ RemediationLevel,
+ ReportConfidence,
+ ConfidentialityRequirement,
+ IntegrityRequirement,
+ AvailabilityRequirement,
+ ModifiedAttackVector,
+ ModifiedAttackComplexity,
+ ModifiedPrivilegesRequired,
+ ModifiedUserInteraction,
+ ModifiedScope,
+ ModifiedConfidentiality,
+ ModifiedIntegrity,
+ ModifiedAvailability,
+) {
+ // If input validation fails, this array is populated with strings indicating which metrics failed validation.
+ var badMetrics = [];
+
+ // ENSURE ALL BASE METRICS ARE DEFINED
+ //
+ // We need values for all Base Score metrics to calculate scores.
+ // If any Base Score parameters are undefined, create an array of missing metrics and return it with an error.
+
+ if (typeof AttackVector === 'undefined' || AttackVector === '') {
+ badMetrics.push('AV');
+ }
+ if (typeof AttackComplexity === 'undefined' || AttackComplexity === '') {
+ badMetrics.push('AC');
+ }
+ if (typeof PrivilegesRequired === 'undefined' || PrivilegesRequired === '') {
+ badMetrics.push('PR');
+ }
+ if (typeof UserInteraction === 'undefined' || UserInteraction === '') {
+ badMetrics.push('UI');
+ }
+ if (typeof Scope === 'undefined' || Scope === '') {
+ badMetrics.push('S');
+ }
+ if (typeof Confidentiality === 'undefined' || Confidentiality === '') {
+ badMetrics.push('C');
+ }
+ if (typeof Integrity === 'undefined' || Integrity === '') {
+ badMetrics.push('I');
+ }
+ if (typeof Availability === 'undefined' || Availability === '') {
+ badMetrics.push('A');
+ }
+
+ if (badMetrics.length > 0) {
+ return {
+ success: false,
+ errorType: 'MissingBaseMetric',
+ errorMetrics: badMetrics,
+ };
+ }
+
+ // STORE THE METRIC VALUES THAT WERE PASSED AS PARAMETERS
+ //
+ // Temporal and Environmental metrics are optional, so set them to "X" ("Not Defined") if no value was passed.
+
+ var AV = AttackVector;
+ var AC = AttackComplexity;
+ var PR = PrivilegesRequired;
+ var UI = UserInteraction;
+ var S = Scope;
+ var C = Confidentiality;
+ var I = Integrity;
+ var A = Availability;
+
+ var E = ExploitCodeMaturity || 'X';
+ var RL = RemediationLevel || 'X';
+ var RC = ReportConfidence || 'X';
+
+ var CR = ConfidentialityRequirement || 'X';
+ var IR = IntegrityRequirement || 'X';
+ var AR = AvailabilityRequirement || 'X';
+ var MAV = ModifiedAttackVector || 'X';
+ var MAC = ModifiedAttackComplexity || 'X';
+ var MPR = ModifiedPrivilegesRequired || 'X';
+ var MUI = ModifiedUserInteraction || 'X';
+ var MS = ModifiedScope || 'X';
+ var MC = ModifiedConfidentiality || 'X';
+ var MI = ModifiedIntegrity || 'X';
+ var MA = ModifiedAvailability || 'X';
+
+ // CHECK VALIDITY OF METRIC VALUES
+ //
+ // Use the Weight object to ensure that, for every metric, the metric value passed is valid.
+ // If any invalid values are found, create an array of their metrics and return it with an error.
+ //
+ // The Privileges Required (PR) weight depends on Scope, but when checking the validity of PR we must not assume
+ // that the given value for Scope is valid. We therefore always look at the weights for Unchanged Scope when
+ // performing this check. The same applies for validation of Modified Privileges Required (MPR).
+ //
+ // The Weights object does not contain "X" ("Not Defined") values for Environmental metrics because we replace them
+ // with their Base metric equivalents later in the function. For example, an MAV of "X" will be replaced with the
+ // value given for AV. We therefore need to explicitly allow a value of "X" for Environmental metrics.
+
+ if (!CVSS31.Weight.AV.hasOwnProperty(AV)) {
+ badMetrics.push('AV');
+ }
+ if (!CVSS31.Weight.AC.hasOwnProperty(AC)) {
+ badMetrics.push('AC');
+ }
+ if (!CVSS31.Weight.PR.U.hasOwnProperty(PR)) {
+ badMetrics.push('PR');
+ }
+ if (!CVSS31.Weight.UI.hasOwnProperty(UI)) {
+ badMetrics.push('UI');
+ }
+ if (!CVSS31.Weight.S.hasOwnProperty(S)) {
+ badMetrics.push('S');
+ }
+ if (!CVSS31.Weight.CIA.hasOwnProperty(C)) {
+ badMetrics.push('C');
+ }
+ if (!CVSS31.Weight.CIA.hasOwnProperty(I)) {
+ badMetrics.push('I');
+ }
+ if (!CVSS31.Weight.CIA.hasOwnProperty(A)) {
+ badMetrics.push('A');
+ }
+
+ if (!CVSS31.Weight.E.hasOwnProperty(E)) {
+ badMetrics.push('E');
+ }
+ if (!CVSS31.Weight.RL.hasOwnProperty(RL)) {
+ badMetrics.push('RL');
+ }
+ if (!CVSS31.Weight.RC.hasOwnProperty(RC)) {
+ badMetrics.push('RC');
+ }
+
+ if (!(CR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(CR))) {
+ badMetrics.push('CR');
+ }
+ if (!(IR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(IR))) {
+ badMetrics.push('IR');
+ }
+ if (!(AR === 'X' || CVSS31.Weight.CIAR.hasOwnProperty(AR))) {
+ badMetrics.push('AR');
+ }
+ if (!(MAV === 'X' || CVSS31.Weight.AV.hasOwnProperty(MAV))) {
+ badMetrics.push('MAV');
+ }
+ if (!(MAC === 'X' || CVSS31.Weight.AC.hasOwnProperty(MAC))) {
+ badMetrics.push('MAC');
+ }
+ if (!(MPR === 'X' || CVSS31.Weight.PR.U.hasOwnProperty(MPR))) {
+ badMetrics.push('MPR');
+ }
+ if (!(MUI === 'X' || CVSS31.Weight.UI.hasOwnProperty(MUI))) {
+ badMetrics.push('MUI');
+ }
+ if (!(MS === 'X' || CVSS31.Weight.S.hasOwnProperty(MS))) {
+ badMetrics.push('MS');
+ }
+ if (!(MC === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MC))) {
+ badMetrics.push('MC');
+ }
+ if (!(MI === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MI))) {
+ badMetrics.push('MI');
+ }
+ if (!(MA === 'X' || CVSS31.Weight.CIA.hasOwnProperty(MA))) {
+ badMetrics.push('MA');
+ }
+
+ if (badMetrics.length > 0) {
+ return {
+ success: false,
+ errorType: 'UnknownMetricValue',
+ errorMetrics: badMetrics,
+ };
+ }
+
+ // GATHER WEIGHTS FOR ALL METRICS
+
+ var metricWeightAV = CVSS31.Weight.AV[AV];
+ var metricWeightAC = CVSS31.Weight.AC[AC];
+ var metricWeightPR = CVSS31.Weight.PR[S][PR]; // PR depends on the value of Scope (S).
+ var metricWeightUI = CVSS31.Weight.UI[UI];
+ var metricWeightS = CVSS31.Weight.S[S];
+ var metricWeightC = CVSS31.Weight.CIA[C];
+ var metricWeightI = CVSS31.Weight.CIA[I];
+ var metricWeightA = CVSS31.Weight.CIA[A];
+
+ var metricWeightE = CVSS31.Weight.E[E];
+ var metricWeightRL = CVSS31.Weight.RL[RL];
+ var metricWeightRC = CVSS31.Weight.RC[RC];
+
+ // For metrics that are modified versions of Base Score metrics, e.g. Modified Attack Vector, use the value of
+ // the Base Score metric if the modified version value is "X" ("Not Defined").
+ var metricWeightCR = CVSS31.Weight.CIAR[CR];
+ var metricWeightIR = CVSS31.Weight.CIAR[IR];
+ var metricWeightAR = CVSS31.Weight.CIAR[AR];
+ var metricWeightMAV = CVSS31.Weight.AV[MAV !== 'X' ? MAV : AV];
+ var metricWeightMAC = CVSS31.Weight.AC[MAC !== 'X' ? MAC : AC];
+ var metricWeightMPR =
+ CVSS31.Weight.PR[MS !== 'X' ? MS : S][MPR !== 'X' ? MPR : PR]; // Depends on MS.
+ var metricWeightMUI = CVSS31.Weight.UI[MUI !== 'X' ? MUI : UI];
+ var metricWeightMS = CVSS31.Weight.S[MS !== 'X' ? MS : S];
+ var metricWeightMC = CVSS31.Weight.CIA[MC !== 'X' ? MC : C];
+ var metricWeightMI = CVSS31.Weight.CIA[MI !== 'X' ? MI : I];
+ var metricWeightMA = CVSS31.Weight.CIA[MA !== 'X' ? MA : A];
+
+ // CALCULATE THE CVSS BASE SCORE
+
+ var iss; /* Impact Sub-Score */
+ var impact;
+ var exploitability;
+ var baseScore;
+
+ iss = 1 - (1 - metricWeightC) * (1 - metricWeightI) * (1 - metricWeightA);
+
+ if (S === 'U') {
+ impact = metricWeightS * iss;
+ } else {
+ impact = metricWeightS * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15);
+ }
+
+ exploitability =
+ CVSS31.exploitabilityCoefficient *
+ metricWeightAV *
+ metricWeightAC *
+ metricWeightPR *
+ metricWeightUI;
+
+ if (impact <= 0) {
+ baseScore = 0;
+ } else {
+ if (S === 'U') {
+ baseScore = CVSS31.roundUp1(Math.min(exploitability + impact, 10));
+ } else {
+ baseScore = CVSS31.roundUp1(
+ Math.min(CVSS31.scopeCoefficient * (exploitability + impact), 10),
+ );
+ }
+ }
+
+ // CALCULATE THE CVSS TEMPORAL SCORE
+
+ var temporalScore = CVSS31.roundUp1(
+ baseScore * metricWeightE * metricWeightRL * metricWeightRC,
+ );
+
+ // CALCULATE THE CVSS ENVIRONMENTAL SCORE
+ //
+ // - modifiedExploitability recalculates the Base Score Exploitability sub-score using any modified values from the
+ // Environmental metrics group in place of the values specified in the Base Score, if any have been defined.
+ // - modifiedImpact recalculates the Base Score Impact sub-score using any modified values from the
+ // Environmental metrics group in place of the values specified in the Base Score, and any additional weightings
+ // given in the Environmental metrics group.
+
+ var miss; /* Modified Impact Sub-Score */
+ var modifiedImpact;
+ var envScore;
+ var modifiedExploitability;
+
+ miss = Math.min(
+ 1 -
+ (1 - metricWeightMC * metricWeightCR) *
+ (1 - metricWeightMI * metricWeightIR) *
+ (1 - metricWeightMA * metricWeightAR),
+ 0.915,
+ );
+
+ if (MS === 'U' || (MS === 'X' && S === 'U')) {
+ modifiedImpact = metricWeightMS * miss;
+ } else {
+ modifiedImpact =
+ metricWeightMS * (miss - 0.029) -
+ 3.25 * Math.pow(miss * 0.9731 - 0.02, 13);
+ }
+
+ modifiedExploitability =
+ CVSS31.exploitabilityCoefficient *
+ metricWeightMAV *
+ metricWeightMAC *
+ metricWeightMPR *
+ metricWeightMUI;
+
+ if (modifiedImpact <= 0) {
+ envScore = 0;
+ } else if (MS === 'U' || (MS === 'X' && S === 'U')) {
+ envScore = CVSS31.roundUp1(
+ CVSS31.roundUp1(Math.min(modifiedImpact + modifiedExploitability, 10)) *
+ metricWeightE *
+ metricWeightRL *
+ metricWeightRC,
+ );
+ } else {
+ envScore = CVSS31.roundUp1(
+ CVSS31.roundUp1(
+ Math.min(
+ CVSS31.scopeCoefficient * (modifiedImpact + modifiedExploitability),
+ 10,
+ ),
+ ) *
+ metricWeightE *
+ metricWeightRL *
+ metricWeightRC,
+ );
+ }
+
+ // CONSTRUCT THE VECTOR STRING
+
+ var vectorString =
+ CVSS31.CVSSVersionIdentifier +
+ '/AV:' +
+ AV +
+ '/AC:' +
+ AC +
+ '/PR:' +
+ PR +
+ '/UI:' +
+ UI +
+ '/S:' +
+ S +
+ '/C:' +
+ C +
+ '/I:' +
+ I +
+ '/A:' +
+ A;
+
+ if (E !== 'X') {
+ vectorString = vectorString + '/E:' + E;
+ }
+ if (RL !== 'X') {
+ vectorString = vectorString + '/RL:' + RL;
+ }
+ if (RC !== 'X') {
+ vectorString = vectorString + '/RC:' + RC;
+ }
+
+ if (CR !== 'X') {
+ vectorString = vectorString + '/CR:' + CR;
+ }
+ if (IR !== 'X') {
+ vectorString = vectorString + '/IR:' + IR;
+ }
+ if (AR !== 'X') {
+ vectorString = vectorString + '/AR:' + AR;
+ }
+ if (MAV !== 'X') {
+ vectorString = vectorString + '/MAV:' + MAV;
+ }
+ if (MAC !== 'X') {
+ vectorString = vectorString + '/MAC:' + MAC;
+ }
+ if (MPR !== 'X') {
+ vectorString = vectorString + '/MPR:' + MPR;
+ }
+ if (MUI !== 'X') {
+ vectorString = vectorString + '/MUI:' + MUI;
+ }
+ if (MS !== 'X') {
+ vectorString = vectorString + '/MS:' + MS;
+ }
+ if (MC !== 'X') {
+ vectorString = vectorString + '/MC:' + MC;
+ }
+ if (MI !== 'X') {
+ vectorString = vectorString + '/MI:' + MI;
+ }
+ if (MA !== 'X') {
+ vectorString = vectorString + '/MA:' + MA;
+ }
+
+ // Return an object containing the scores for all three metric groups, and an overall vector string.
+ // Sub-formula values are also included.
+
+ return {
+ success: true,
+
+ baseMetricScore: baseScore.toFixed(1),
+ baseSeverity: CVSS31.severityRating(baseScore.toFixed(1)),
+ baseISS: iss,
+ baseImpact: impact,
+ baseExploitability: exploitability,
+
+ temporalMetricScore: temporalScore.toFixed(1),
+ temporalSeverity: CVSS31.severityRating(temporalScore.toFixed(1)),
+
+ environmentalMetricScore: envScore.toFixed(1),
+ environmentalSeverity: CVSS31.severityRating(envScore.toFixed(1)),
+ environmentalMISS: miss,
+ environmentalModifiedImpact: modifiedImpact,
+ environmentalModifiedExploitability: modifiedExploitability,
+
+ vectorString: vectorString,
+ };
+};
+
+/* ** CVSS31.calculateCVSSFromVector **
+ *
+ * Takes Base, Temporal and Environmental metric values as a single string in the Vector String format defined
+ * in the CVSS v3.1 standard definition of the Vector String.
+ *
+ * Returns Base, Temporal and Environmental scores, severity ratings, and an overall Vector String. All Base metrics
+ * are required to generate this output. All Temporal and Environmental metric values are optional. Any that are not
+ * passed default to "X" ("Not Defined").
+ *
+ * See the comment for the CVSS31.calculateCVSSFromMetrics function for details on the function output. In addition to
+ * the error conditions listed for that function, this function can also return:
+ * "MalformedVectorString", if the Vector String passed does not conform to the format in the standard; or
+ * "MultipleDefinitionsOfMetric", if the Vector String is well formed but defines the same metric (or metrics),
+ * more than once.
+ */
+CVSS31.calculateCVSSFromVector = function (vectorString) {
+ var metricValues = {
+ AV: undefined,
+ AC: undefined,
+ PR: undefined,
+ UI: undefined,
+ S: undefined,
+ C: undefined,
+ I: undefined,
+ A: undefined,
+ E: undefined,
+ RL: undefined,
+ RC: undefined,
+ CR: undefined,
+ IR: undefined,
+ AR: undefined,
+ MAV: undefined,
+ MAC: undefined,
+ MPR: undefined,
+ MUI: undefined,
+ MS: undefined,
+ MC: undefined,
+ MI: undefined,
+ MA: undefined,
+ };
+
+ // If input validation fails, this array is populated with strings indicating which metrics failed validation.
+ var badMetrics = [];
+
+ if (!CVSS31.vectorStringRegex_31.test(vectorString)) {
+ return { success: false, errorType: 'MalformedVectorString' };
+ }
+
+ var metricNameValue = vectorString
+ .substring(CVSS31.CVSSVersionIdentifier.length)
+ .split('/');
+
+ for (var i in metricNameValue) {
+ if (metricNameValue.hasOwnProperty(i)) {
+ var singleMetric = metricNameValue[i].split(':');
+
+ if (typeof metricValues[singleMetric[0]] === 'undefined') {
+ metricValues[singleMetric[0]] = singleMetric[1];
+ } else {
+ badMetrics.push(singleMetric[0]);
+ }
+ }
+ }
+
+ if (badMetrics.length > 0) {
+ return {
+ success: false,
+ errorType: 'MultipleDefinitionsOfMetric',
+ errorMetrics: badMetrics,
+ };
+ }
+
+ return CVSS31.calculateCVSSFromMetrics(
+ metricValues.AV,
+ metricValues.AC,
+ metricValues.PR,
+ metricValues.UI,
+ metricValues.S,
+ metricValues.C,
+ metricValues.I,
+ metricValues.A,
+ metricValues.E,
+ metricValues.RL,
+ metricValues.RC,
+ metricValues.CR,
+ metricValues.IR,
+ metricValues.AR,
+ metricValues.MAV,
+ metricValues.MAC,
+ metricValues.MPR,
+ metricValues.MUI,
+ metricValues.MS,
+ metricValues.MC,
+ metricValues.MI,
+ metricValues.MA,
+ );
+};
+
+/* ** CVSS31.roundUp1 **
+ *
+ * Rounds up its parameter to 1 decimal place and returns the result.
+ *
+ * Standard JavaScript errors thrown when arithmetic operations are performed on non-numbers will be returned if the
+ * given input is not a number.
+ *
+ * Implementation note: Tiny representation errors in floating point numbers makes rounding complex. For example,
+ * consider calculating Math.ceil((1-0.58)*100) by hand. It can be simplified to Math.ceil(0.42*100), then
+ * Math.ceil(42), and finally 42. Most JavaScript implementations give 43. The problem is that, on many systems,
+ * 1-0.58 = 0.42000000000000004, and the tiny error is enough to push ceil up to the next integer. The implementation
+ * below avoids such problems by performing the rounding using integers. The input is first multiplied by 100,000
+ * and rounded to the nearest integer to consider 6 decimal places of accuracy, so 0.000001 results in 0.0, but
+ * 0.000009 results in 0.1.
+ *
+ * A more elegant solution may be possible, but the following gives answers consistent with results from an arbitrary
+ * precision library.
+ */
+CVSS31.roundUp1 = function Roundup(input) {
+ var int_input = Math.round(input * 100000);
+
+ if (int_input % 10000 === 0) {
+ return int_input / 100000;
+ } else {
+ return (Math.floor(int_input / 10000) + 1) / 10;
+ }
+};
+
+/* ** CVSS31.severityRating **
+ *
+ * Given a CVSS score, returns the name of the severity rating as defined in the CVSS standard.
+ * The input needs to be a number between 0.0 to 10.0, to one decimal place of precision.
+ *
+ * The following error values may be returned instead of a severity rating name:
+ * NaN (JavaScript "Not a Number") - if the input is not a number.
+ * undefined - if the input is a number that is not within the range of any defined severity rating.
+ */
+CVSS31.severityRating = function (score) {
+ var severityRatingLength = CVSS31.severityRatings.length;
+
+ var validatedScore = Number(score);
+
+ if (isNaN(validatedScore)) {
+ return validatedScore;
+ }
+
+ for (var i = 0; i < severityRatingLength; i++) {
+ if (
+ score >= CVSS31.severityRatings[i].bottom &&
+ score <= CVSS31.severityRatings[i].top
+ ) {
+ return CVSS31.severityRatings[i].name;
+ }
+ }
+
+ return undefined;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// DATA AND FUNCTIONS FOR CREATING AN XML REPRESENTATION OF A CVSS SCORE //
+///////////////////////////////////////////////////////////////////////////
+
+// A mapping between abbreviated metric values and the string used in the XML representation.
+// For example, a Remediation Level (RL) abbreviated metric value of "W" maps to "WORKAROUND".
+// For brevity, every Base metric shares its definition with its equivalent Environmental metric. This is possible
+// because the metric values are same between these groups, except that the latter have an additional metric value
+// of "NOT_DEFINED".
+
+CVSS31.XML_MetricNames = {
+ E: {
+ X: 'NOT_DEFINED',
+ U: 'UNPROVEN',
+ P: 'PROOF_OF_CONCEPT',
+ F: 'FUNCTIONAL',
+ H: 'HIGH',
+ },
+ RL: {
+ X: 'NOT_DEFINED',
+ O: 'OFFICIAL_FIX',
+ T: 'TEMPORARY_FIX',
+ W: 'WORKAROUND',
+ U: 'UNAVAILABLE',
+ },
+ RC: { X: 'NOT_DEFINED', U: 'UNKNOWN', R: 'REASONABLE', C: 'CONFIRMED' },
+
+ CIAR: { X: 'NOT_DEFINED', L: 'LOW', M: 'MEDIUM', H: 'HIGH' }, // CR, IR and AR use the same values
+ MAV: {
+ N: 'NETWORK',
+ A: 'ADJACENT_NETWORK',
+ L: 'LOCAL',
+ P: 'PHYSICAL',
+ X: 'NOT_DEFINED',
+ },
+ MAC: { H: 'HIGH', L: 'LOW', X: 'NOT_DEFINED' },
+ MPR: { N: 'NONE', L: 'LOW', H: 'HIGH', X: 'NOT_DEFINED' },
+ MUI: { N: 'NONE', R: 'REQUIRED', X: 'NOT_DEFINED' },
+ MS: { U: 'UNCHANGED', C: 'CHANGED', X: 'NOT_DEFINED' },
+ MCIA: { N: 'NONE', L: 'LOW', H: 'HIGH', X: 'NOT_DEFINED' }, // C, I and A use the same values
+};
+
+/* ** CVSS31.generateXMLFromMetrics **
+ *
+ * Takes Base, Temporal and Environmental metric values as individual parameters. Their values are in the short format
+ * defined in the CVSS v3.1 standard definition of the Vector String. For example, the AttackComplexity parameter
+ * should be either "H" or "L".
+ *
+ * Returns a single string containing the metric values in XML form. All Base metrics are required to generate this
+ * output. All Temporal and Environmental metric values are optional. Any that are not passed will be represented in
+ * the XML as NOT_DEFINED. The function returns a string for simplicity. It is arguably better to return the XML as
+ * a DOM object, but at the time of writing this leads to complexity due to older browsers using different JavaScript
+ * interfaces to do this. Also for simplicity, all Temporal and Environmental metrics are included in the string,
+ * even though those with a value of "Not Defined" do not need to be included.
+ *
+ * The output of this function is an object which always has a property named "success".
+ *
+ * If no errors are encountered, success is Boolean "true", and the "xmlString" property contains the XML string
+ * representation.
+ *
+ * If errors are encountered, success is Boolean "false", and other properties are defined as per the
+ * CVSS31.calculateCVSSFromMetrics function. Refer to the comment for that function for more details.
+ */
+CVSS31.generateXMLFromMetrics = function (
+ AttackVector,
+ AttackComplexity,
+ PrivilegesRequired,
+ UserInteraction,
+ Scope,
+ Confidentiality,
+ Integrity,
+ Availability,
+ ExploitCodeMaturity,
+ RemediationLevel,
+ ReportConfidence,
+ ConfidentialityRequirement,
+ IntegrityRequirement,
+ AvailabilityRequirement,
+ ModifiedAttackVector,
+ ModifiedAttackComplexity,
+ ModifiedPrivilegesRequired,
+ ModifiedUserInteraction,
+ ModifiedScope,
+ ModifiedConfidentiality,
+ ModifiedIntegrity,
+ ModifiedAvailability,
+) {
+ // A string containing the XML we wish to output, with placeholders for the CVSS metrics we will substitute for
+ // their values, based on the inputs passed to this function.
+ var xmlTemplate =
+ '\n' +
+ '
${html}
`; + var doc = new docx.Document({ sections: [] }); + var paragraphs = []; + var cParagraph = null; + var cRunProperties = {}; + var cParagraphProperties = {}; + var list_state = []; + var inCodeBlock = false; + var parser = new htmlparser.Parser( + { + onopentag(tag, attribs) { + if (tag === 'h1') { + cParagraph = new docx.Paragraph({ heading: 'Heading1' }); + } else if (tag === 'h2') { + cParagraph = new docx.Paragraph({ heading: 'Heading2' }); + } else if (tag === 'h3') { + cParagraph = new docx.Paragraph({ heading: 'Heading3' }); + } else if (tag === 'h4') { + cParagraph = new docx.Paragraph({ heading: 'Heading4' }); + } else if (tag === 'h5') { + cParagraph = new docx.Paragraph({ heading: 'Heading5' }); + } else if (tag === 'h6') { + cParagraph = new docx.Paragraph({ heading: 'Heading6' }); + } else if (tag === 'div' || tag === 'p') { + if (style && typeof style === 'string') + cParagraphProperties.style = style; + cParagraph = new docx.Paragraph(cParagraphProperties); + } else if (tag === 'pre') { + inCodeBlock = true; + cParagraph = new docx.Paragraph({ style: 'Code' }); + } else if (tag === 'b' || tag === 'strong') { + cRunProperties.bold = true; + } else if (tag === 'i' || tag === 'em') { + cRunProperties.italics = true; + } else if (tag === 'u') { + cRunProperties.underline = {}; + } else if (tag === 'strike' || tag === 's') { + cRunProperties.strike = true; + } else if (tag === 'mark') { + var bgColor = attribs['data-color'] || '#ffff25'; + cRunProperties.highlight = getHighlightColor(bgColor); + + // Use text color if set (to handle white or black text depending on background color) + var color = attribs.style.match(/.+color:.(.+)/); + if (color && color[1]) cRunProperties.color = getTextColor(color[1]); + } else if (tag === 'br') { + if (inCodeBlock) { + paragraphs.push(cParagraph); + cParagraph = new docx.Paragraph({ style: 'Code' }); + } else cParagraph.addChildElement(new docx.Run({ break: 1 })); + } else if (tag === 'ul') { + list_state.push('bullet'); + } else if (tag === 'ol') { + list_state.push('number'); + } else if (tag === 'li') { + var level = list_state.length - 1; + if (level >= 0 && list_state[level] === 'bullet') + cParagraphProperties.bullet = { level: level }; + else if (level >= 0 && list_state[level] === 'number') + cParagraphProperties.numbering = { reference: 2, level: level }; + else cParagraphProperties.bullet = { level: 0 }; + } else if (tag === 'code') { + cRunProperties.style = 'CodeChar'; + } else if (tag === 'legend' && attribs && attribs.alt !== 'undefined') { + var label = attribs.label || 'Figure'; + cParagraph = new docx.Paragraph({ + style: 'Caption', + alignment: docx.AlignmentType.CENTER, + }); + cParagraph.addChildElement(new docx.TextRun(`${label} `)); + cParagraph.addChildElement(new docx.SimpleField(`SEQ ${label}`, '1')); + cParagraph.addChildElement(new docx.TextRun(` - ${attribs.alt}`)); + } + }, + + ontext(text) { + if (text && cParagraph) { + cRunProperties.text = text; + cParagraph.addChildElement(new docx.TextRun(cRunProperties)); + } + }, + + onclosetag(tag) { + if ( + [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'div', + 'p', + 'pre', + 'img', + 'legend', + ].includes(tag) + ) { + paragraphs.push(cParagraph); + cParagraph = null; + cParagraphProperties = {}; + if (tag === 'pre') inCodeBlock = false; + } else if (tag === 'b' || tag === 'strong') { + delete cRunProperties.bold; + } else if (tag === 'i' || tag === 'em') { + delete cRunProperties.italics; + } else if (tag === 'u') { + delete cRunProperties.underline; + } else if (tag === 'strike' || tag === 's') { + delete cRunProperties.strike; + } else if (tag === 'mark') { + delete cRunProperties.highlight; + delete cRunProperties.color; + } else if (tag === 'ul' || tag === 'ol') { + list_state.pop(); + if (list_state.length === 0) cParagraphProperties = {}; + } else if (tag === 'code') { + delete cRunProperties.style; + } + }, + + onend() { + doc.addSection({ + children: paragraphs, + }); + }, + }, + { decodeEntities: true }, + ); + + // For multiline code blocks + html = html.replace(/\n/g, '') == 0) { + return input.substring(3, input.length - 4).split('
');
+ } else {
+ return input.split('\n');
+ }
+};
+
+// Creates a hyperlink: {@input | linkTo: 'https://example.com' | p}
+expressions.filters.linkTo = function (input, url) {
+ return (
+ '
<\/p>)+$/, ''), style);
+ return result;
+};
+
+// Count vulnerability by severity
+// Example: {findings | count: 'Critical'}
+expressions.filters.count = function (input, severity) {
+ if (!input) return input;
+ var count = 0;
+
+ for (var i = 0; i < input.length; i++) {
+ if (input[i].cvss.baseSeverity === severity) {
+ count += 1;
+ }
+ }
+
+ return count;
+};
+
+// Translate using locale from 'translate' folder
+// Example: {input | translate: 'fr'}
+expressions.filters.translate = function (input, locale) {
+ translate.setLocale(locale);
+ if (!input) return input;
+ return translate.translate(input, locale);
+};
+
+module.exports = expressions;
diff --git a/backend/src/lib/report-generator.js b/backend/src/lib/report-generator.js
new file mode 100644
index 0000000000000000000000000000000000000000..2cc0813311c5de3b78befa72435cf2928cc7b37c
--- /dev/null
+++ b/backend/src/lib/report-generator.js
@@ -0,0 +1,707 @@
+var fs = require('fs');
+var Docxtemplater = require('docxtemplater');
+var PizZip = require('pizzip');
+var expressions = require('./report-filters');
+var ImageModule = require('docxtemplater-image-module-pwndoc');
+var sizeOf = require('image-size');
+var customGenerator = require('./custom-generator');
+var utils = require('./utils');
+var _ = require('lodash');
+var Image = require('mongoose').model('Image');
+const libre = require('libreoffice-convert');
+const { parseAsync } = require('json2csv');
+var Settings = require('mongoose').model('Settings');
+var CVSS31 = require('./cvsscalc31.js');
+var translate = require('../translate');
+var $t;
+const muhammara = require('muhammara');
+const path = require('path');
+const os = require('os');
+const { v4: uuidv4 } = require('uuid');
+
+// Generate document with docxtemplater
+async function generateDoc(audit) {
+ var templatePath = `${__basedir}/../report-templates/${audit.template.name}.${audit.template.ext || 'docx'}`;
+ var content = fs.readFileSync(templatePath, 'binary');
+
+ var zip = new PizZip(content);
+
+ translate.setLocale(audit.language);
+ $t = translate.translate;
+
+ var settings = await Settings.getAll();
+ var preppedAudit = await prepAuditData(audit, settings);
+
+ var opts = {};
+ // opts.centered = true;
+ opts.getImage = function (tagValue, tagName) {
+ if (tagValue !== 'undefined') {
+ tagValue = tagValue.split(',')[1];
+ return Buffer.from(tagValue, 'base64');
+ }
+ // return fs.readFileSync(tagValue, {encoding: 'base64'});
+ };
+ opts.getSize = function (img, tagValue, tagName) {
+ if (img) {
+ var sizeObj = sizeOf(img);
+ var width = sizeObj.width;
+ var height = sizeObj.height;
+ if (tagName === 'company.logo_small') {
+ var divider = sizeObj.height / 37;
+ height = 37;
+ width = Math.floor(sizeObj.width / divider);
+ } else if (tagName === 'company.logo') {
+ var divider = sizeObj.height / 250;
+ height = 250;
+ width = Math.floor(sizeObj.width / divider);
+ if (width > 400) {
+ divider = sizeObj.width / 400;
+ height = Math.floor(sizeObj.height / divider);
+ width = 400;
+ }
+ } else if (sizeObj.width > 600) {
+ var divider = sizeObj.width / 600;
+ width = 600;
+ height = Math.floor(sizeObj.height / divider);
+ }
+ return [width, height];
+ }
+ return [0, 0];
+ };
+
+ if (
+ settings.report.private.imageBorder &&
+ settings.report.private.imageBorderColor
+ )
+ opts.border = settings.report.private.imageBorderColor.replace('#', '');
+
+ try {
+ var imageModule = new ImageModule(opts);
+ } catch (err) {
+ console.log(err);
+ }
+ var doc = new Docxtemplater()
+ .attachModule(imageModule)
+ .loadZip(zip)
+ .setOptions({ parser: parser, paragraphLoop: true });
+ customGenerator.apply(preppedAudit);
+ doc.setData(preppedAudit);
+ try {
+ doc.render();
+ } catch (error) {
+ if (error.properties.id === 'multi_error') {
+ error.properties.errors.forEach(function (err) {
+ console.log(err);
+ });
+ } else console.log(error);
+ if (error.properties && error.properties.errors instanceof Array) {
+ const errorMessages = error.properties.errors
+ .map(function (error) {
+ return `Explanation: ${error.properties.explanation}\nScope: ${JSON.stringify(error.properties.scope).substring(0, 142)}...`;
+ })
+ .join('\n\n');
+ // errorMessages is a humanly readable message looking like this :
+ // 'The tag beginning with "foobar" is unopened'
+ throw `Template Error:\n${errorMessages}`;
+ } else {
+ throw error;
+ }
+ }
+ var buf = doc.getZip().generate({ type: 'nodebuffer' });
+
+ return buf;
+}
+exports.generateDoc = generateDoc;
+
+// Generates a PDF from a docx using libreoffice-convert
+// libreoffice-convert leverages libreoffice to convert office documents to different formats
+// https://www.npmjs.com/package/libreoffice-convert
+async function generatePdf(audit) {
+ var docxReport = await generateDoc(audit);
+ return new Promise((resolve, reject) =>
+ libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
+ if (err) console.log(err);
+ resolve(pdf);
+ }),
+ );
+}
+exports.generatePdf = generatePdf;
+
+// Generates a encrypted PDF using libreoffice-convert
+// and muhammara to encrypt it with a given password.
+// https://www.npmjs.com/package/muhammara
+
+async function generateEncryptedPdf(audit, password) {
+ // Genera el archivo DOCX
+ var docxReport = await generateDoc(audit);
+
+ return new Promise((resolve, reject) => {
+ libre.convert(docxReport, '.pdf', undefined, (err, pdf) => {
+ if (err) {
+ console.log(err);
+ return reject(err);
+ }
+
+ const tempPdfPath = path.join(
+ os.tmpdir(),
+ `documento_sin_contraseña_${uuidv4()}.pdf`,
+ );
+ fs.writeFileSync(tempPdfPath, pdf);
+
+ const protectedPdfPath = path.join(
+ os.tmpdir(),
+ `documento_protegido_${uuidv4()}.pdf`,
+ );
+
+ try {
+ const Recipe = muhammara.Recipe;
+ const pdfDoc = new Recipe(tempPdfPath, protectedPdfPath);
+
+ pdfDoc
+ .encrypt({
+ userPassword: password,
+ ownerPassword: password,
+ userProtectionFlag: 4,
+ })
+ .endPDF();
+
+ const protectedPdf = fs.readFileSync(protectedPdfPath);
+
+ fs.unlinkSync(tempPdfPath);
+ fs.unlinkSync(protectedPdfPath);
+
+ resolve(protectedPdf);
+ } catch (error) {
+ console.error('Error protecting PDF:', error);
+ reject(error);
+ }
+ });
+ });
+}
+exports.generateEncryptedPdf = generateEncryptedPdf;
+
+// Generates a csv from the json data
+// Leverages json2csv
+// https://www.npmjs.com/package/json2csv
+async function generateCsv(audit) {
+ return parseAsync(audit._doc);
+}
+exports.generateCsv = generateCsv;
+
+// Filters helper: handles the use of preformated easilly translatable strings.
+// Source: https://www.tutorialstonight.com/javascript-string-format.php
+String.prototype.format = function () {
+ let args = arguments;
+ return this.replace(/{([0-9]+)}/g, function (match, index) {
+ return typeof args[index] == 'undefined' ? match : args[index];
+ });
+};
+
+// Compile all angular expressions
+var angularParser = function (tag) {
+ expressions = { ...expressions, ...customGenerator.expressions };
+ if (tag === '.') {
+ return {
+ get: function (s) {
+ return s;
+ },
+ };
+ }
+ const expr = expressions.compile(
+ tag.replace(/(’|‘)/g, "'").replace(/(“|”)/g, '"'),
+ );
+ return {
+ get: function (scope, context) {
+ let obj = {};
+ const scopeList = context.scopeList;
+ const num = context.num;
+ for (let i = 0, len = num + 1; i < len; i++) {
+ obj = _.merge(obj, scopeList[i]);
+ }
+ return expr(scope, obj);
+ },
+ };
+};
+
+function parser(tag) {
+ // We write an exception to handle the tag "$pageBreakExceptLast"
+ if (tag === '$pageBreakExceptLast') {
+ return {
+ get(scope, context) {
+ const totalLength =
+ context.scopePathLength[context.scopePathLength.length - 1];
+ const index = context.scopePathItem[context.scopePathItem.length - 1];
+ const isLast = index === totalLength - 1;
+ if (!isLast) {
+ return ' Paragraph Text Paragraph Bold Paragraph Italic Paragraph Underline Paragraph Paragraph Mark Paragraph Bullet1 Bullet2 Bullet1 Bullet2 Bullet1 BulletNested Bullet2 Number1 Number2 Number1 NumberNested Number2 Paragraph Paragraph\nBreak Paragraph
+
+ AuditForge (PwnDoc fork) is a pentest reporting application making it simple and easy to write your findings and generate a customizable Docx report.
+ The main goal is to have more time to search vulnerabilities and less time to write documentation by mutualizing data like vulnerabilities between users.
+
+ Website
+
+
+
+
+
+ "===e?(m=new ae(h(h({},m),{name:W()})),H()):E.test(e)||x.test(e)||":"===e||$()}function w(e){">"===e?$():E.test(e)?f=3:$()}function _(e){S.test(e)||("/"===e?f=12:">"===e?H():"<"===e?V():"="===e||A.test(e)||C.test(e)?$():f=5)}function k(e){S.test(e)?f=6:"/"===e?f=12:"="===e?f=7:">"===e?H():"<"===e?V():A.test(e)&&$()}function O(e){S.test(e)||("/"===e?f=12:"="===e?f=7:">"===e?H():"<"===e?V():A.test(e)?$():f=5)}function j(e){S.test(e)||('"'===e?f=8:"'"===e?f=9:/[>=`]/.test(e)?$():"<"===e?V():f=10)}function T(e){'"'===e&&(f=11)}function I(e){"'"===e&&(f=11)}function N(e){S.test(e)?f=4:">"===e?H():"<"===e&&V()}function P(e){S.test(e)?f=4:"/"===e?f=12:">"===e?H():"<"===e?V():(f=4,c--)}function R(e){">"===e?(m=new ae(h(h({},m),{isClosing:!0})),H()):f=4}function M(t){"--"===e.substr(c,2)?(c+=2,m=new ae(h(h({},m),{type:"comment"})),f=14):"DOCTYPE"===e.substr(c,7).toUpperCase()?(c+=7,m=new ae(h(h({},m),{type:"doctype"})),f=20):$()}function D(e){"-"===e?f=15:">"===e?$():f=16}function L(e){"-"===e?f=18:">"===e?$():f=16}function B(e){"-"===e&&(f=17)}function F(e){f="-"===e?18:16}function U(e){">"===e?H():"!"===e?f=19:"-"===e||(f=16)}function z(e){"-"===e?f=17:">"===e?H():f=16}function q(e){">"===e?H():"<"===e&&V()}function $(){f=0,m=u}function V(){f=1,m=new ae({idx:c})}function H(){var t=e.slice(d,m.idx);t&&a(t,d),"comment"===m.type?i(m.idx):"doctype"===m.type?l(m.idx):(m.isOpening&&r(m.name,m.idx),m.isClosing&&o(m.name,m.idx)),$(),d=c+1}function W(){var t=m.idx+(m.isClosing?2:1);return e.slice(t,c).toLowerCase()}d >16&255,u[c++]=t>>8&255,u[c++]=255&t;2===s&&(t=r[e.charCodeAt(n)]<<2|r[e.charCodeAt(n+1)]>>4,u[c++]=255&t);1===s&&(t=r[e.charCodeAt(n)]<<10|r[e.charCodeAt(n+1)]<<4|r[e.charCodeAt(n+2)]>>2,u[c++]=t>>8&255,u[c++]=255&t);return u},t.fromByteArray=function(e){for(var t,r=e.length,o=r%3,a=[],i=16383,s=0,l=r-o;s 1) src = src[1];
+ if (alt && alt.length > 1) alt = _.unescape(alt[1]);
+
+ if (!src.startsWith('data')) {
+ try {
+ src = (await Image.getOne(src)).value;
+ } catch (error) {
+ src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=';
+ }
+ }
+ if (result.length === 0) result.push({ text: '', images: [] });
+ result[result.length - 1].images.push({ image: src, caption: alt });
+ } else if (value === '') {
+ continue;
+ } else {
+ result.push({ text: value, images: [] });
+ }
+ }
+ return result;
+}
+
+function replaceSubTemplating(o, originalData = o) {
+ var regexp = /\{_\{([a-zA-Z0-9\[\]\_\.]{1,})\}_\}/gm;
+ if (Array.isArray(o))
+ o.forEach(key => replaceSubTemplating(key, originalData));
+ else if (typeof o === 'object' && !!o) {
+ Object.keys(o).forEach(key => {
+ if (typeof o[key] === 'string')
+ o[key] = o[key].replace(regexp, (match, word) =>
+ _.get(originalData, word.trim(), ''),
+ );
+ else replaceSubTemplating(o[key], originalData);
+ });
+ }
+}
diff --git a/backend/src/lib/utils.js b/backend/src/lib/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..89b74c6963b67342e466ea1f4ed2f40e734701c9
--- /dev/null
+++ b/backend/src/lib/utils.js
@@ -0,0 +1,58 @@
+// Filename whitelist validation for template creation
+function validFilename(filename) {
+ const regex = /^[\p{Letter}\p{Mark}0-9 \[\]'()_-]+$/iu;
+
+ return regex.test(filename);
+}
+exports.validFilename = validFilename;
+
+// Escape XML special entities when using {@RawXML} in template generation
+function escapeXMLEntities(input) {
+ var XML_CHAR_MAP = { '<': '<', '>': '>', '&': '&' };
+ var standardEncode = input.replace(/[<>&]/g, function (ch) {
+ return XML_CHAR_MAP[ch];
+ });
+ return standardEncode;
+}
+exports.escapeXMLEntities = escapeXMLEntities;
+
+// Convert number to 3 digits format if under 100
+function lPad(number) {
+ if (number <= 99) {
+ number = ('00' + number).slice(-3);
+ }
+ return `${number}`;
+}
+exports.lPad = lPad;
+
+function escapeRegex(regex) {
+ return regex.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+exports.escapeRegex = escapeRegex;
+
+function generateUUID() {
+ return require('crypto').randomBytes(32).toString('hex');
+}
+exports.generateUUID = generateUUID;
+
+var getObjectPaths = (obj, prefix = '') =>
+ Object.keys(obj).reduce((res, el) => {
+ if (Array.isArray(obj[el])) {
+ return [...res, prefix + el];
+ } else if (typeof obj[el] === 'object' && obj[el] !== null) {
+ return [...res, ...getObjectPaths(obj[el], prefix + el + '.')];
+ }
+ return [...res, prefix + el];
+ }, []);
+exports.getObjectPaths = getObjectPaths;
+
+function getSockets(io, room) {
+ var result = [];
+ io.sockets.sockets.forEach(data => {
+ if (data.rooms.has(room)) {
+ result.push(data);
+ }
+ });
+ return result;
+}
+exports.getSockets = getSockets;
diff --git a/backend/src/models/audit-type.js b/backend/src/models/audit-type.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bda8be6b6a4bc6675a84c3feee27b0b95229c46
--- /dev/null
+++ b/backend/src/models/audit-type.js
@@ -0,0 +1,114 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var Template = {
+ _id: false,
+ template: { type: Schema.Types.ObjectId, ref: 'Template' },
+ locale: String,
+};
+
+var AuditTypeSchema = new Schema(
+ {
+ name: { type: String, unique: true },
+ templates: [Template],
+ sections: [{ type: String, ref: 'CustomSection' }],
+ hidden: [{ type: String, enum: ['network', 'findings'] }],
+ stage: {
+ type: String,
+ enum: ['default', 'retest', 'multi'],
+ default: 'default',
+ },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all auditTypes
+AuditTypeSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = AuditType.find();
+ query.select('_id name templates sections hidden stage');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get auditType by name
+AuditTypeSchema.statics.getByName = name => {
+ return new Promise((resolve, reject) => {
+ var query = AuditType.findOne({ name: name });
+ query.select('-_id name templates sections hidden stage');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create auditType
+AuditTypeSchema.statics.create = auditType => {
+ return new Promise((resolve, reject) => {
+ var query = new AuditType(auditType);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Audit Type already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Update Audit Types
+AuditTypeSchema.statics.updateAll = auditTypes => {
+ return new Promise((resolve, reject) => {
+ AuditType.deleteMany()
+ .then(row => {
+ AuditType.insertMany(auditTypes);
+ })
+ .then(row => {
+ resolve('Audit Types updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete auditType
+AuditTypeSchema.statics.delete = name => {
+ return new Promise((resolve, reject) => {
+ AuditType.deleteOne({ name: name })
+ .then(res => {
+ if (res.deletedCount === 1) resolve('Audit Type deleted');
+ else reject({ fn: 'NotFound', message: 'Audit Type not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var AuditType = mongoose.model('AuditType', AuditTypeSchema);
+AuditType.syncIndexes();
+module.exports = AuditType;
diff --git a/backend/src/models/audit.js b/backend/src/models/audit.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5d14eecd63ea987e9c5cebf64b31755e8de7cc6
--- /dev/null
+++ b/backend/src/models/audit.js
@@ -0,0 +1,1195 @@
+var mongoose = require('mongoose'); //.set('debug', true);
+const CVSS31 = require('../lib/cvsscalc31');
+var Schema = mongoose.Schema;
+
+var Paragraph = {
+ text: String,
+ images: [{ image: String, caption: String }],
+};
+
+var customField = {
+ _id: false,
+ customField: { type: Schema.Types.Mixed, ref: 'CustomField' },
+ text: Schema.Types.Mixed,
+};
+
+var Finding = {
+ id: Schema.Types.ObjectId,
+ identifier: Number, //incremental ID to be shown in the report
+ title: String,
+ vulnType: String,
+ description: String,
+ observation: String,
+ remediation: String,
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
+ priority: { type: Number, enum: [1, 2, 3, 4] },
+ references: [String],
+ cwes: [String],
+ cvssv3: String,
+ paragraphs: [Paragraph],
+ poc: String,
+ scope: String,
+ status: { type: Number, enum: [0, 1], default: 1 }, // 0: done, 1: redacting
+ category: String,
+ customFields: [customField],
+ retestStatus: { type: String, enum: ['ok', 'ko', 'unknown', 'partial'] },
+ retestDescription: String,
+};
+
+var Service = {
+ port: Number,
+ protocol: { type: String, enum: ['tcp', 'udp'] },
+ name: String,
+ product: String,
+ version: String,
+};
+
+var Host = {
+ hostname: String,
+ ip: String,
+ os: String,
+ services: [Service],
+};
+
+var SortOption = {
+ _id: false,
+ category: String,
+ sortValue: String,
+ sortOrder: { type: String, enum: ['desc', 'asc'] },
+ sortAuto: Boolean,
+};
+
+var AuditSchema = new Schema(
+ {
+ name: { type: String, required: true },
+ auditType: String,
+ date: String,
+ date_start: String,
+ date_end: String,
+ summary: String,
+ company: { type: Schema.Types.ObjectId, ref: 'Company' },
+ client: { type: Schema.Types.ObjectId, ref: 'Client' },
+ collaborators: [{ type: Schema.Types.ObjectId, ref: 'User' }],
+ reviewers: [{ type: Schema.Types.ObjectId, ref: 'User' }],
+ language: { type: String, required: true },
+ scope: [{ _id: false, name: String, hosts: [Host] }],
+ findings: [Finding],
+ template: { type: Schema.Types.ObjectId, ref: 'Template' },
+ creator: { type: Schema.Types.ObjectId, ref: 'User' },
+ sections: [
+ {
+ field: String,
+ name: String,
+ text: String,
+ customFields: [customField],
+ },
+ ], // keep text for retrocompatibility
+ customFields: [customField],
+ sortFindings: [SortOption],
+ state: {
+ type: String,
+ enum: ['EDIT', 'REVIEW', 'APPROVED'],
+ default: 'EDIT',
+ },
+ approvals: [{ type: Schema.Types.ObjectId, ref: 'User' }],
+ type: {
+ type: String,
+ enum: ['default', 'multi', 'retest'],
+ default: 'default',
+ },
+ parentId: { type: Schema.Types.ObjectId, ref: 'Audit' },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all audits (admin)
+AuditSchema.statics.getAudits = (isAdmin, userId, filters) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.find(filters);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query.populate('creator', 'username');
+ query.populate('collaborators', 'username');
+ query.populate('reviewers', 'username firstname lastname');
+ query.populate('approvals', 'username firstname lastname');
+ query.populate('company', 'name');
+ query.populate('template', '-_id ext');
+ query.select(
+ 'id name auditType language creator collaborators company createdAt state type parentId template',
+ );
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get Audit with ID to generate report
+AuditSchema.statics.getAudit = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query.populate('template');
+ query.populate('creator', 'username firstname lastname email phone role');
+ query.populate('company');
+ query.populate('client');
+ query.populate(
+ 'collaborators',
+ 'username firstname lastname email phone role',
+ );
+ query.populate('reviewers', 'username firstname lastname role');
+ query.populate('approvals', 'username firstname lastname role');
+ query.populate('customFields.customField', 'label fieldType text');
+ query.populate({
+ path: 'findings',
+ populate: {
+ path: 'customFields.customField',
+ select: 'label fieldType text',
+ },
+ });
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.name === 'CastError')
+ reject({ fn: 'BadParameters', message: 'Bad Audit Id' });
+ else reject(err);
+ });
+ });
+};
+
+AuditSchema.statics.getAuditChildren = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.find({ parentId: auditId });
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query
+ .exec()
+ .then(rows => {
+ if (!rows)
+ throw {
+ fn: 'NotFound',
+ message: 'Children not found or Insufficient Privileges',
+ };
+ resolve(rows);
+ })
+ .catch(err => {
+ if (err.name === 'CastError')
+ reject({ fn: 'BadParameters', message: 'Bad Audit Id' });
+ else reject(err);
+ });
+ });
+};
+
+// Get Audit Retest
+AuditSchema.statics.getRetest = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findOne({ parentId: auditId });
+
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw { fn: 'NotFound', message: 'No retest found for this audit' };
+ else {
+ resolve(row);
+ }
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create Audit Retest
+AuditSchema.statics.createRetest = (isAdmin, auditId, userId, auditType) => {
+ return new Promise((resolve, reject) => {
+ var audit = {};
+ audit.creator = userId;
+ audit.type = 'retest';
+ audit.parentId = auditId;
+ audit.auditType = auditType;
+ audit.findings = [];
+ audit.sections = [];
+ audit.customFields = [];
+
+ var auditTypeSections = [];
+ var customSections = [];
+ var customFields = [];
+ var AuditType = mongoose.model('AuditType');
+
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query
+ .exec()
+ .then(async row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+ else {
+ var retest = await Audit.findOne({ parentId: auditId }).exec();
+ if (retest)
+ throw {
+ fn: 'BadParameters',
+ message: 'Retest already exists for this Audit',
+ };
+ audit.name = row.name;
+ audit.company = row.company;
+ audit.client = row.client;
+ audit.collaborators = row.collaborators;
+ audit.reviewers = row.reviewers;
+ audit.language = row.language;
+ audit.scope = row.scope;
+ audit.findings = row.findings;
+ // row.findings.forEach(finding => {
+ // var tmpFinding = {}
+ // tmpFinding.title = finding.title
+ // tmpFinding.identifier = finding.identifier
+ // tmpFinding.cvssv3 = finding.cvssv3
+ // tmpFinding.vulnType = finding.vulnType
+ // tmpFinding.category = finding.category
+ // audit.findings.push(tmpFinding)
+ // })
+ return AuditType.getByName(auditType);
+ }
+ })
+ .then(row => {
+ if (row) {
+ auditTypeSections = row.sections;
+ var auditTypeTemplate = row.templates.find(
+ e => e.locale === audit.language,
+ );
+ if (auditTypeTemplate) audit.template = auditTypeTemplate.template;
+ var Section = mongoose.model('CustomSection');
+ var CustomField = mongoose.model('CustomField');
+ var promises = [];
+ promises.push(Section.getAll());
+ promises.push(CustomField.getAll());
+ return Promise.all(promises);
+ } else throw { fn: 'NotFound', message: 'AuditType not found' };
+ })
+ .then(resolved => {
+ customSections = resolved[0];
+ customFields = resolved[1];
+
+ customSections.forEach(section => {
+ // Add sections with customFields (and default text) to audit
+ var tmpSection = {};
+ if (auditTypeSections.includes(section.field)) {
+ tmpSection.field = section.field;
+ tmpSection.name = section.name;
+ tmpSection.customFields = [];
+
+ customFields.forEach(field => {
+ field = field.toObject();
+ if (
+ field.display === 'section' &&
+ field.displaySub === tmpSection.name
+ ) {
+ var fieldText = field.text.find(
+ e => e.locale === audit.language,
+ );
+ if (fieldText) fieldText = fieldText.value;
+ else fieldText = '';
+
+ delete field.text;
+ tmpSection.customFields.push({
+ customField: field,
+ text: fieldText,
+ });
+ }
+ });
+ audit.sections.push(tmpSection);
+ }
+ });
+
+ customFields.forEach(field => {
+ // Add customFields (and default text) to audit
+ field = field.toObject();
+ if (field.display === 'general') {
+ var fieldText = field.text.find(e => e.locale === audit.language);
+ if (fieldText) fieldText = fieldText.value;
+ else fieldText = '';
+
+ delete field.text;
+ audit.customFields.push({ customField: field, text: fieldText });
+ }
+ });
+
+ return new Audit(audit).save();
+ })
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ console.log(err);
+ if (err.name === 'ValidationError')
+ reject({ fn: 'BadParameters', message: 'Audit validation failed' });
+ else reject(err);
+ });
+ });
+};
+
+// Create audit
+AuditSchema.statics.create = (audit, userId) => {
+ return new Promise((resolve, reject) => {
+ audit.creator = userId;
+ audit.sections = [];
+ audit.customFields = [];
+
+ var auditTypeSections = [];
+ var customSections = [];
+ var customFields = [];
+ var AuditType = mongoose.model('AuditType');
+ AuditType.getByName(audit.auditType)
+ .then(row => {
+ if (row) {
+ auditTypeSections = row.sections;
+ var auditTypeTemplate = row.templates.find(
+ e => e.locale === audit.language,
+ );
+ if (auditTypeTemplate) audit.template = auditTypeTemplate.template;
+ var Section = mongoose.model('CustomSection');
+ var CustomField = mongoose.model('CustomField');
+ var promises = [];
+ promises.push(Section.getAll());
+ promises.push(CustomField.getAll());
+ return Promise.all(promises);
+ } else throw { fn: 'NotFound', message: 'AuditType not found' };
+ })
+ .then(resolved => {
+ customSections = resolved[0];
+ customFields = resolved[1];
+
+ customSections.forEach(section => {
+ // Add sections with customFields (and default text) to audit
+ var tmpSection = {};
+ if (auditTypeSections.includes(section.field)) {
+ tmpSection.field = section.field;
+ tmpSection.name = section.name;
+ tmpSection.customFields = [];
+
+ customFields.forEach(field => {
+ field = field.toObject();
+ if (
+ field.display === 'section' &&
+ field.displaySub === tmpSection.name
+ ) {
+ var fieldText = field.text.find(
+ e => e.locale === audit.language,
+ );
+ if (fieldText) fieldText = fieldText.value;
+ else fieldText = '';
+
+ delete field.text;
+ tmpSection.customFields.push({
+ customField: field,
+ text: fieldText,
+ });
+ }
+ });
+ audit.sections.push(tmpSection);
+ }
+ });
+
+ customFields.forEach(field => {
+ // Add customFields (and default text) to audit
+ field = field.toObject();
+ if (field.display === 'general') {
+ var fieldText = field.text.find(e => e.locale === audit.language);
+ if (fieldText) fieldText = fieldText.value;
+ else fieldText = '';
+
+ delete field.text;
+ audit.customFields.push({ customField: field, text: fieldText });
+ }
+ });
+
+ var VulnerabilityCategory = mongoose.model('VulnerabilityCategory');
+ return VulnerabilityCategory.getAll();
+ })
+ .then(rows => {
+ // Add default sort options for each vulnerability category
+ audit.sortFindings = [];
+ rows.forEach(e => {
+ audit.sortFindings.push({
+ category: e.name,
+ sortValue: e.sortValue,
+ sortOrder: e.sortOrder,
+ sortAuto: e.sortAuto,
+ });
+ });
+
+ return new Audit(audit).save();
+ })
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ console.log(err);
+ if (err.name === 'ValidationError')
+ reject({ fn: 'BadParameters', message: 'Audit validation failed' });
+ else reject(err);
+ });
+ });
+};
+
+// Delete audit
+AuditSchema.statics.delete = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findOneAndDelete({ _id: auditId });
+ if (!isAdmin) query.or([{ creator: userId }]);
+ return query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve(row);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get audit general information
+AuditSchema.statics.getGeneral = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query.populate({
+ path: 'client',
+ select: 'email firstname lastname',
+ populate: {
+ path: 'company',
+ select: 'name',
+ },
+ });
+ query.populate('creator', 'username firstname lastname');
+ query.populate('collaborators', 'username firstname lastname');
+ query.populate('reviewers', 'username firstname lastname');
+ query.populate('company');
+ query.select(
+ 'name auditType date date_start date_end client collaborators language scope.name template customFields',
+ );
+ query
+ .lean()
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var formatScope = row.scope.map(item => {
+ return item.name;
+ });
+ for (var i = 0; i < formatScope.length; i++) {
+ row.scope[i] = formatScope[i];
+ }
+ resolve(row);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update audit general information
+AuditSchema.statics.updateGeneral = (isAdmin, auditId, userId, update) => {
+ return new Promise(async (resolve, reject) => {
+ if (update.company && update.company.name) {
+ var Company = mongoose.model('Company');
+ try {
+ update.company = await Company.create({ name: update.company.name });
+ } catch (error) {
+ console.log(error);
+ delete update.company;
+ }
+ }
+ var query = Audit.findByIdAndUpdate(auditId, update);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve('Audit General updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get audit Network information
+AuditSchema.statics.getNetwork = (isAdmin, auditId, userId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query.select('scope');
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve(row);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update audit Network information
+AuditSchema.statics.updateNetwork = (isAdmin, auditId, userId, scope) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findByIdAndUpdate(auditId, scope);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve('Audit Network updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create finding
+AuditSchema.statics.createFinding = (isAdmin, auditId, userId, finding) => {
+ return new Promise((resolve, reject) => {
+ Audit.getLastFindingIdentifier(auditId).then(identifier => {
+ finding.identifier = ++identifier;
+
+ var query = Audit.findByIdAndUpdate(auditId, {
+ $push: { findings: finding },
+ });
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ return query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+ else {
+ var sortOption = row.sortFindings.find(
+ e => e.category === (finding.category || 'No Category'),
+ );
+ if ((sortOption && sortOption.sortAuto) || !sortOption)
+ // if sort is set to automatic or undefined then we sort (default sort will be applied to undefined sortOption)
+ return Audit.updateSortFindings(isAdmin, auditId, userId, null);
+ // if manual sorting then we do not sort
+ else resolve('Audit Finding created succesfully');
+ }
+ })
+ .then(() => {
+ resolve('Audit Finding created successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+ });
+};
+
+AuditSchema.statics.getLastFindingIdentifier = auditId => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.aggregate([
+ { $match: { _id: new mongoose.Types.ObjectId(auditId) } },
+ ]);
+ query.unwind('findings');
+ query.sort({ 'findings.identifier': -1 });
+ query
+ .exec()
+ .then(row => {
+ if (!row) throw { fn: 'NotFound', message: 'Audit not found' };
+ else if (row.length === 0 || !row[0].findings.identifier) resolve(0);
+ else resolve(row[0].findings.identifier);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get finding of audit
+AuditSchema.statics.getFinding = (isAdmin, auditId, userId, findingId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+ query.select('findings');
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var finding = row.findings.id(findingId);
+ if (finding === null)
+ throw { fn: 'NotFound', message: 'Finding not found' };
+ else resolve(finding);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update finding of audit
+AuditSchema.statics.updateFinding = (
+ isAdmin,
+ auditId,
+ userId,
+ findingId,
+ newFinding,
+) => {
+ return new Promise((resolve, reject) => {
+ var sortAuto = true;
+
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var finding = row.findings.id(findingId);
+ if (finding === null)
+ reject({ fn: 'NotFound', message: 'Finding not found' });
+ else {
+ var sortOption = row.sortFindings.find(
+ e => e.category === (newFinding.category || 'No Category'),
+ );
+ if (sortOption && !sortOption.sortAuto) sortAuto = false;
+
+ Object.keys(newFinding).forEach(key => {
+ finding[key] = newFinding[key];
+ });
+ return row.save({ validateBeforeSave: false }); // Disable schema validation since scope changed from Array to String
+ }
+ })
+ .then(() => {
+ if (sortAuto)
+ return Audit.updateSortFindings(isAdmin, auditId, userId, null);
+ else resolve('Audit Finding updated successfully');
+ })
+ .then(() => {
+ resolve('Audit Finding updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete finding of audit
+AuditSchema.statics.deleteFinding = (isAdmin, auditId, userId, findingId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query.select('findings');
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var finding = row.findings.id(findingId);
+ if (finding === null)
+ reject({ fn: 'NotFound', message: 'Finding not found' });
+ else {
+ row.findings.pull(findingId);
+ return row.save();
+ }
+ })
+ .then(() => {
+ resolve('Audit Finding deleted successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create section
+AuditSchema.statics.createSection = (isAdmin, auditId, userId, section) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findOneAndUpdate(
+ { _id: auditId, 'sections.field': { $ne: section.field } },
+ { $push: { sections: section } },
+ );
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message:
+ 'Audit not found or Section already exists or Insufficient Privileges',
+ };
+
+ resolve('Audit Section created successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get section of audit
+AuditSchema.statics.getSection = (isAdmin, auditId, userId, sectionId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin)
+ query.or([
+ { creator: userId },
+ { collaborators: userId },
+ { reviewers: userId },
+ ]);
+
+ query.select('sections');
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var section = row.sections.id(sectionId);
+ if (section === null)
+ throw { fn: 'NotFound', message: 'Section id not found' };
+ else resolve(section);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update section of audit
+AuditSchema.statics.updateSection = (
+ isAdmin,
+ auditId,
+ userId,
+ sectionId,
+ newSection,
+) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var section = row.sections.id(sectionId);
+ if (section === null)
+ throw { fn: 'NotFound', message: 'Section not found' };
+ else {
+ Object.keys(newSection).forEach(key => {
+ section[key] = newSection[key];
+ });
+ return row.save();
+ }
+ })
+ .then(() => {
+ resolve('Audit Section updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete section of audit
+AuditSchema.statics.deleteSection = (isAdmin, auditId, userId, sectionId) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query.select('sections');
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var section = row.sections.id(sectionId);
+ if (section === null)
+ throw { fn: 'NotFound', message: 'Section not found' };
+ else {
+ row.sections.pull(sectionId);
+ return row.save();
+ }
+ })
+ .then(() => {
+ resolve('Audit Section deleted successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update audit sort options for findings and run the sorting. If update param is null then just run sorting
+(AuditSchema.statics.updateSortFindings = (
+ isAdmin,
+ auditId,
+ userId,
+ update,
+) => {
+ return new Promise((resolve, reject) => {
+ var audit = {};
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+ else {
+ audit = row;
+ if (update)
+ // if update is null then we only sort findings (no sort options saving)
+ audit.sortFindings = update.sortFindings; // saving sort options to audit
+
+ var VulnerabilityCategory = mongoose.model('VulnerabilityCategory');
+ return VulnerabilityCategory.getAll();
+ }
+ })
+ .then(row => {
+ var _ = require('lodash');
+ var findings = [];
+ var categoriesOrder = row.map(e => e.name);
+ categoriesOrder.push('undefined'); // Put uncategorized findings at the end
+
+ // Group findings by category
+ var findingList = _.chain(audit.findings)
+ .groupBy('category')
+ .toPairs()
+ .sort(
+ (a, b) =>
+ categoriesOrder.indexOf(a[0]) - categoriesOrder.indexOf(b[0]),
+ )
+ .fromPairs()
+ .map((value, key) => {
+ if (key === 'undefined') key = 'No Category';
+ var sortOption = audit.sortFindings.find(
+ option => option.category === key,
+ ); // Get sort option saved in audit
+ if (!sortOption)
+ // no option for category in audit
+ sortOption = row.find(e => e.name === key); // Get sort option from default in vulnerability category
+ if (!sortOption)
+ // no default option or category don't exist
+ sortOption = {
+ sortValue: 'cvssScore',
+ sortOrder: 'desc',
+ sortAuto: true,
+ }; // set a default sort option
+
+ return { category: key, findings: value, sortOption: sortOption };
+ })
+ .value();
+
+ findingList.forEach(group => {
+ var order = -1; // desc
+ if (group.sortOption.sortOrder === 'asc') order = 1;
+
+ var tmpFindings = group.findings.sort((a, b) => {
+ var cvssA = CVSS31.calculateCVSSFromVector(a.cvssv3);
+ var cvssB = CVSS31.calculateCVSSFromVector(b.cvssv3);
+
+ // Get built-in value (findings[sortValue])
+ var left = a[group.sortOption.sortValue];
+
+ // If sort value is a CVSS Score calculate it
+ if (cvssA.success && group.sortOption.sortValue === 'cvssScore')
+ left = cvssA.baseMetricScore;
+ else if (
+ cvssA.success &&
+ group.sortOption.sortValue === 'cvssTemporalScore'
+ )
+ left = cvssA.temporalMetricScore;
+ else if (
+ cvssA.success &&
+ group.sortOption.sortValue === 'cvssEnvironmentalScore'
+ )
+ left = cvssA.environmentalMetricScore;
+
+ // Not found then get customField sortValue
+ if (!left) {
+ left = a.customFields.find(
+ e => e.customField.label === group.sortOption.sortValue,
+ );
+ if (left) left = left.text;
+ }
+ // Not found then set default to 0
+ if (!left) left = 0;
+ // Convert to string in case of int value
+ left = left.toString();
+
+ // Same for right value to compare
+ var right = b[group.sortOption.sortValue];
+
+ if (cvssB.success && group.sortOption.sortValue === 'cvssScore')
+ right = cvssB.baseMetricScore;
+ else if (
+ cvssB.success &&
+ group.sortOption.sortValue === 'cvssTemporalScore'
+ )
+ right = cvssB.temporalMetricScore;
+ else if (
+ cvssB.success &&
+ group.sortOption.sortValue === 'cvssEnvironmentalScore'
+ )
+ right = cvssB.environmentalMetricScore;
+
+ if (!right) {
+ right = b.customFields.find(
+ e => e.customField.label === group.sortOption.sortValue,
+ );
+ if (right) right = right.text;
+ }
+ if (!right) right = 0;
+ right = right.toString();
+ return (
+ left.localeCompare(right, undefined, { numeric: true }) * order
+ );
+ });
+
+ findings = findings.concat(tmpFindings);
+ });
+
+ audit.findings = findings;
+
+ return audit.save();
+ })
+ .then(() => {
+ resolve('Audit findings sorted successfully');
+ })
+ .catch(err => {
+ console.log(err);
+ reject(err);
+ });
+ });
+}),
+ // Move finding from move.oldIndex to move.newIndex
+ (AuditSchema.statics.moveFindingPosition = (
+ isAdmin,
+ auditId,
+ userId,
+ move,
+ ) => {
+ return new Promise((resolve, reject) => {
+ var query = Audit.findById(auditId);
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ var tmp = row.findings[move.oldIndex];
+ row.findings.splice(move.oldIndex, 1);
+ row.findings.splice(move.newIndex, 0, tmp);
+
+ row.markModified('findings');
+ return row.save();
+ })
+ .then(msg => {
+ resolve('Audit Finding moved successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+ });
+
+AuditSchema.statics.updateApprovals = (isAdmin, auditId, userId, update) => {
+ return new Promise(async (resolve, reject) => {
+ var Settings = mongoose.model('Settings');
+ var settings = await Settings.getAll();
+
+ if (update.approvals.length >= settings.reviews.public.minReviewers) {
+ update.state = 'APPROVED';
+ } else {
+ update.state = 'REVIEW';
+ }
+
+ var query = Audit.findByIdAndUpdate(auditId, update);
+ query.nor([{ creator: userId }, { collaborators: userId }]);
+ if (!isAdmin) query.or([{ reviewers: userId }]);
+
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve('Audit approvals updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Update audit parent
+AuditSchema.statics.updateParent = (isAdmin, auditId, userId, parentId) => {
+ return new Promise(async (resolve, reject) => {
+ var query = Audit.findByIdAndUpdate(auditId, { parentId: parentId });
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve('Audit Parent updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete audit parent
+AuditSchema.statics.deleteParent = (isAdmin, auditId, userId) => {
+ return new Promise(async (resolve, reject) => {
+ var query = Audit.findByIdAndUpdate(auditId, { parentId: null });
+ if (!isAdmin) query.or([{ creator: userId }, { collaborators: userId }]);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ throw {
+ fn: 'NotFound',
+ message: 'Audit not found or Insufficient Privileges',
+ };
+
+ resolve(row);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Audit = mongoose.model('Audit', AuditSchema);
+// Audit.syncIndexes()
+module.exports = Audit;
diff --git a/backend/src/models/client.js b/backend/src/models/client.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ac9f39228fcfab4961ecb7cc8c1489db18f786f
--- /dev/null
+++ b/backend/src/models/client.js
@@ -0,0 +1,128 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var ClientSchema = new Schema(
+ {
+ email: { type: String, required: true, unique: true },
+ company: { type: Schema.Types.ObjectId, ref: 'Company' },
+ lastname: String,
+ firstname: String,
+ phone: String,
+ cell: String,
+ title: String,
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all clients
+ClientSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Client.find().populate('company', '-_id name');
+ query.select('email lastname firstname phone cell title');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create client
+ClientSchema.statics.create = (client, company) => {
+ return new Promise(async (resolve, reject) => {
+ if (company) {
+ var Company = mongoose.model('Company');
+ var query = Company.findOneAndUpdate(
+ { name: company },
+ {},
+ { upsert: true, new: true },
+ );
+ var companyRow = await query.exec();
+ if (companyRow) client.company = companyRow._id;
+ }
+ var query = new Client(client);
+ query
+ .save(company)
+ .then(row => {
+ resolve({
+ _id: row._id,
+ email: row.email,
+ firstname: row.firstname,
+ lastname: row.lastname,
+ title: row.title,
+ phone: row.phone,
+ cell: row.cell,
+ company: row.company,
+ });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Client email already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update client
+ClientSchema.statics.update = (clientId, client, company) => {
+ return new Promise(async (resolve, reject) => {
+ if (company) {
+ var Company = mongoose.model('Company');
+ var query = Company.findOneAndUpdate(
+ { name: company },
+ {},
+ { upsert: true, new: true },
+ );
+ var companyRow = await query.exec();
+ if (companyRow) client.company = companyRow.id;
+ }
+ var query = Client.findOneAndUpdate({ _id: clientId }, client);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Client Id not found' });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Client email already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Delete client
+ClientSchema.statics.delete = clientId => {
+ return new Promise((resolve, reject) => {
+ var query = Client.findOneAndDelete({ _id: clientId });
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Client Id not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Client = mongoose.model('Client', ClientSchema);
+module.exports = Client;
diff --git a/backend/src/models/company.js b/backend/src/models/company.js
new file mode 100644
index 0000000000000000000000000000000000000000..e5b748b8daeca4a78fc3a52b56b319706e9c1dfc
--- /dev/null
+++ b/backend/src/models/company.js
@@ -0,0 +1,95 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var CompanySchema = new Schema(
+ {
+ name: { type: String, required: true, unique: true },
+ shortName: String,
+ logo: String,
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all companies
+CompanySchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Company.find();
+ query.select('name shortName logo');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create company
+CompanySchema.statics.create = company => {
+ return new Promise((resolve, reject) => {
+ var query = new Company(company);
+ query
+ .save(company)
+ .then(row => {
+ resolve({ _id: row._id, name: row.name });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Company name already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update company
+CompanySchema.statics.update = (companyId, company) => {
+ return new Promise((resolve, reject) => {
+ var query = Company.findOneAndUpdate({ _id: companyId }, company);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Company Id not found' });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Company name already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Delete company
+CompanySchema.statics.delete = companyId => {
+ return new Promise((resolve, reject) => {
+ var query = Company.findOneAndDelete({ _id: companyId });
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Company Id not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Company = mongoose.model('Company', CompanySchema);
+module.exports = Company;
diff --git a/backend/src/models/custom-field.js b/backend/src/models/custom-field.js
new file mode 100644
index 0000000000000000000000000000000000000000..99d2eddbeb0bd058cb301c2ddc1635e518441d46
--- /dev/null
+++ b/backend/src/models/custom-field.js
@@ -0,0 +1,147 @@
+var mongoose = require('mongoose'); //.set('debug', true);
+var Schema = mongoose.Schema;
+
+var CustomFieldSchema = new Schema(
+ {
+ fieldType: String,
+ label: String,
+ display: String,
+ displaySub: { type: String, default: '' },
+ position: Number,
+ size: {
+ type: Number,
+ enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ default: 12,
+ },
+ offset: {
+ type: Number,
+ enum: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
+ default: 0,
+ },
+ required: { type: Boolean, default: false },
+ description: { type: String, default: '' },
+ text: [{ _id: false, locale: String, value: Schema.Types.Mixed }],
+ options: [{ _id: false, locale: String, value: String }],
+ },
+ { timestamps: true },
+);
+
+CustomFieldSchema.index(
+ { label: 1, display: 1, displaySub: 1 },
+ {
+ name: 'unique_label_display',
+ unique: true,
+ partialFilterExpression: { label: { $exists: true, $gt: '' } },
+ },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all Fields
+CustomFieldSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = CustomField.find().sort('position');
+ query.select(
+ 'fieldType label display displaySub size offset required description text options',
+ );
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create Field
+CustomFieldSchema.statics.create = field => {
+ return new Promise((resolve, reject) => {
+ var query = new CustomField(field);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Custom Field already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update Fields
+CustomFieldSchema.statics.updateAll = fields => {
+ return new Promise((resolve, reject) => {
+ var promises = fields.map(field => {
+ return CustomField.findByIdAndUpdate(field._id, field).exec();
+ });
+ return Promise.all(promises)
+ .then(row => {
+ resolve('Fields updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete Field
+CustomFieldSchema.statics.delete = fieldId => {
+ return new Promise((resolve, reject) => {
+ var pullCount = 0;
+ var Vulnerability = mongoose.model('Vulnerability');
+ var query = Vulnerability.find();
+ query
+ .exec()
+ .then(rows => {
+ var promises = [];
+ promises.push(CustomField.findByIdAndDelete(fieldId).exec());
+ rows.map(row => {
+ row.details.map(detail => {
+ if (
+ detail.customFields.some(
+ field => `${field.customField}` === fieldId,
+ )
+ )
+ pullCount++;
+
+ detail.customFields.pull({ customField: fieldId });
+ });
+ promises.push(row.save());
+ });
+ return Promise.all(promises);
+ })
+ .then(row => {
+ if (row && row[0])
+ resolve({
+ msg: `Custom Field deleted successfully`,
+ vulnCount: pullCount,
+ });
+ else
+ reject({
+ fn: 'NotFound',
+ message: { msg: 'Custom Field not found', vulnCount: pullCount },
+ });
+ })
+ .catch(err => {
+ console.log(err);
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var CustomField = mongoose.model('CustomField', CustomFieldSchema);
+CustomField.syncIndexes();
+module.exports = CustomField;
diff --git a/backend/src/models/custom-section.js b/backend/src/models/custom-section.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f5c7c9919178bd48b5148ac96d403981f02ac6b
--- /dev/null
+++ b/backend/src/models/custom-section.js
@@ -0,0 +1,105 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var CustomSectionSchema = new Schema(
+ {
+ field: { type: String, required: true, unique: true },
+ name: { type: String, required: true, unique: true },
+ icon: String,
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all Sections
+CustomSectionSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = CustomSection.find();
+ query.select('-_id field name icon');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get all Sections by Language
+CustomSectionSchema.statics.getAllByLanguage = locale => {
+ return new Promise((resolve, reject) => {
+ var query = CustomSection.find({ locale: locale });
+ query.select('-_id field name icon');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create Section
+CustomSectionSchema.statics.create = section => {
+ return new Promise((resolve, reject) => {
+ var query = new CustomSection(section);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Custom Section already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update Sections
+CustomSectionSchema.statics.updateAll = sections => {
+ return new Promise((resolve, reject) => {
+ CustomSection.deleteMany()
+ .then(row => {
+ CustomSection.insertMany(sections);
+ })
+ .then(row => {
+ resolve('Sections updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete Section
+CustomSectionSchema.statics.delete = (field, locale) => {
+ return new Promise((resolve, reject) => {
+ CustomSection.deleteOne({ field: field, locale: locale })
+ .then(res => {
+ if (res.deletedCount === 1) resolve('Custom Section deleted');
+ else reject({ fn: 'NotFound', message: 'Custom Section not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var CustomSection = mongoose.model('CustomSection', CustomSectionSchema);
+CustomSection.syncIndexes();
+module.exports = CustomSection;
diff --git a/backend/src/models/image.js b/backend/src/models/image.js
new file mode 100644
index 0000000000000000000000000000000000000000..255961a310940f6de2d8c8866c9e2e91023ee7af
--- /dev/null
+++ b/backend/src/models/image.js
@@ -0,0 +1,78 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var ImageSchema = new Schema(
+ {
+ auditId: { type: Schema.Types.ObjectId, ref: 'Audit' },
+ value: { type: String, required: true, unique: true },
+ name: String,
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get one image
+ImageSchema.statics.getOne = imageId => {
+ return new Promise((resolve, reject) => {
+ var query = Image.findById(imageId);
+
+ query.select('auditId value name');
+ query
+ .exec()
+ .then(row => {
+ if (row) resolve(row);
+ else throw { fn: 'NotFound', message: 'Image not found' };
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create image
+ImageSchema.statics.create = image => {
+ return new Promise((resolve, reject) => {
+ var query = Image.findOne({ value: image.value });
+ query
+ .exec()
+ .then(row => {
+ if (row) return row;
+ query = new Image(image);
+ return query.save();
+ })
+ .then(row => {
+ resolve({ _id: row._id });
+ })
+ .catch(err => {
+ console.log(err);
+ reject(err);
+ });
+ });
+};
+
+// Delete image
+ImageSchema.statics.delete = imageId => {
+ return new Promise((resolve, reject) => {
+ var query = Image.findByIdAndDelete(imageId);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Image not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Image = mongoose.model('Image', ImageSchema);
+Image.syncIndexes();
+module.exports = Image;
diff --git a/backend/src/models/language.js b/backend/src/models/language.js
new file mode 100644
index 0000000000000000000000000000000000000000..05f3aaa39dc87230b0bbb1e82886c7029f1e9a8a
--- /dev/null
+++ b/backend/src/models/language.js
@@ -0,0 +1,84 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var LanguageSchema = new Schema(
+ {
+ language: { type: String, unique: true },
+ locale: { type: String, unique: true },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all languages
+LanguageSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Language.find();
+ query.select('-_id language locale');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create language
+LanguageSchema.statics.create = language => {
+ return new Promise((resolve, reject) => {
+ var query = new Language(language);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Language already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Update languages
+LanguageSchema.statics.updateAll = languages => {
+ return new Promise((resolve, reject) => {
+ Language.deleteMany()
+ .then(row => {
+ Language.insertMany(languages);
+ })
+ .then(row => {
+ resolve('Languages updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete language
+LanguageSchema.statics.delete = locale => {
+ return new Promise((resolve, reject) => {
+ Language.deleteOne({ locale: locale })
+ .then(res => {
+ if (res.deletedCount === 1) resolve('Language deleted');
+ else reject({ fn: 'NotFound', message: 'Language not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Language = mongoose.model('Language', LanguageSchema);
+module.exports = Language;
diff --git a/backend/src/models/settings.js b/backend/src/models/settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..8a825a18f2f7ac697de03b0ff67064b9edb8c042
--- /dev/null
+++ b/backend/src/models/settings.js
@@ -0,0 +1,188 @@
+var mongoose = require('mongoose'); //.set('debug', true);
+var Schema = mongoose.Schema;
+var _ = require('lodash');
+var Utils = require('../lib/utils.js');
+
+// https://stackoverflow.com/questions/25822289/what-is-the-best-way-to-store-color-hex-values-in-mongodb-mongoose
+const colorValidator = v => /^#([0-9a-f]{3}){1,2}$/i.test(v);
+
+const SettingSchema = new Schema(
+ {
+ report: {
+ enabled: { type: Boolean, default: true },
+ public: {
+ cvssColors: {
+ noneColor: {
+ type: String,
+ default: '#4a86e8',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ lowColor: {
+ type: String,
+ default: '#008000',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ mediumColor: {
+ type: String,
+ default: '#f9a009',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ highColor: {
+ type: String,
+ default: '#fe0000',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ criticalColor: {
+ type: String,
+ default: '#212121',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ },
+ captions: {
+ type: [{ type: String, unique: true }],
+ default: ['Figure'],
+ },
+ highlightWarning: { type: Boolean, default: false },
+ highlightWarningColor: {
+ type: String,
+ default: '#ffff25',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ requiredFields: {
+ company: { type: Boolean, default: false },
+ client: { type: Boolean, default: false },
+ dateStart: { type: Boolean, default: false },
+ dateEnd: { type: Boolean, default: false },
+ dateReport: { type: Boolean, default: false },
+ scope: { type: Boolean, default: false },
+ findingType: { type: Boolean, default: false },
+ findingDescription: { type: Boolean, default: false },
+ findingObservation: { type: Boolean, default: false },
+ findingReferences: { type: Boolean, default: false },
+ findingProofs: { type: Boolean, default: false },
+ findingAffected: { type: Boolean, default: false },
+ findingRemediationDifficulty: { type: Boolean, default: false },
+ findingPriority: { type: Boolean, default: false },
+ findingRemediation: { type: Boolean, default: false },
+ },
+ },
+ private: {
+ imageBorder: { type: Boolean, default: false },
+ imageBorderColor: {
+ type: String,
+ default: '#000000',
+ validate: [colorValidator, 'Invalid color'],
+ },
+ },
+ },
+ reviews: {
+ enabled: { type: Boolean, default: false },
+ public: {
+ mandatoryReview: { type: Boolean, default: false },
+ minReviewers: {
+ type: Number,
+ default: 1,
+ min: 1,
+ max: 100,
+ validate: [Number.isInteger, 'Invalid integer'],
+ },
+ },
+ private: {
+ removeApprovalsUponUpdate: { type: Boolean, default: false },
+ },
+ },
+ },
+ { strict: true },
+);
+
+// Get all settings
+SettingSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ const query = Settings.findOne({});
+ query.select('-_id -__v');
+ query
+ .exec()
+ .then(settings => {
+ resolve(settings);
+ })
+ .catch(err => reject(err));
+ });
+};
+
+// Get public settings
+SettingSchema.statics.getPublic = () => {
+ return new Promise((resolve, reject) => {
+ const query = Settings.findOne({});
+ query.select(
+ '-_id report.enabled report.public reviews.enabled reviews.public',
+ );
+ query
+ .exec()
+ .then(settings => resolve(settings))
+ .catch(err => reject(err));
+ });
+};
+
+// Update Settings
+SettingSchema.statics.update = settings => {
+ return new Promise((resolve, reject) => {
+ const query = Settings.findOneAndUpdate({}, settings, {
+ new: true,
+ runValidators: true,
+ });
+ query
+ .exec()
+ .then(settings => resolve(settings))
+ .catch(err => reject(err));
+ });
+};
+
+// Restore settings to default
+SettingSchema.statics.restoreDefaults = () => {
+ return new Promise((resolve, reject) => {
+ const query = Settings.deleteMany({});
+ query
+ .exec()
+ .then(_ => {
+ const query = new Settings({});
+ query
+ .save()
+ .then(_ => resolve('Restored default settings.'))
+ .catch(err => reject(err));
+ })
+ .catch(err => reject(err));
+ });
+};
+
+const Settings = mongoose.model('Settings', SettingSchema);
+
+// Populate/update settings when server starts
+Settings.findOne()
+ .then(liveSettings => {
+ if (!liveSettings) {
+ console.log('Initializing Settings');
+ Settings.create({}).catch(err => {
+ throw 'Error creating the settings in the database : ' + err;
+ });
+ } else {
+ var needUpdate = false;
+ var liveSettingsPaths = Utils.getObjectPaths(liveSettings.toObject());
+
+ liveSettingsPaths.forEach(path => {
+ if (!SettingSchema.path(path) && !path.startsWith('_')) {
+ needUpdate = true;
+ _.set(liveSettings, path, undefined);
+ }
+ });
+
+ if (needUpdate) {
+ console.log('Removing unused fields from Settings');
+ liveSettings.save();
+ }
+ }
+ })
+ .catch(err => {
+ throw 'Error checking for initial settings in the database : ' + err;
+ });
+
+module.exports = Settings;
diff --git a/backend/src/models/template.js b/backend/src/models/template.js
new file mode 100644
index 0000000000000000000000000000000000000000..423b711c54632bdd8f91ced05fb7e4cf4fac5942
--- /dev/null
+++ b/backend/src/models/template.js
@@ -0,0 +1,110 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var TemplateSchema = new Schema(
+ {
+ name: { type: String, required: true, unique: true },
+ ext: { type: String, required: true, unique: false },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all templates
+TemplateSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Template.find();
+ query.select('name ext');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get one template
+TemplateSchema.statics.getOne = templateId => {
+ return new Promise((resolve, reject) => {
+ var query = Template.findById(templateId);
+ query.select('name ext');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create template
+TemplateSchema.statics.create = template => {
+ return new Promise((resolve, reject) => {
+ var query = new Template(template);
+ query
+ .save()
+ .then(row => {
+ resolve({ _id: row._id, name: row.name, ext: row.ext });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Template name already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update template
+TemplateSchema.statics.update = (templateId, template) => {
+ return new Promise((resolve, reject) => {
+ var query = Template.findByIdAndUpdate(templateId, template);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Template not found' });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Template name already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Delete template
+TemplateSchema.statics.delete = templateId => {
+ return new Promise((resolve, reject) => {
+ var query = Template.findByIdAndDelete(templateId);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Template not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Template = mongoose.model('Template', TemplateSchema);
+module.exports = Template;
diff --git a/backend/src/models/user.js b/backend/src/models/user.js
new file mode 100644
index 0000000000000000000000000000000000000000..41071d492c1d317b7a0d0fe866e1f424ca906098
--- /dev/null
+++ b/backend/src/models/user.js
@@ -0,0 +1,430 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+var bcrypt = require('bcrypt');
+var jwt = require('jsonwebtoken');
+
+var auth = require('../lib/auth.js');
+const { generateUUID } = require('../lib/utils.js');
+var _ = require('lodash');
+
+var QRCode = require('qrcode');
+var OTPAuth = require('otpauth');
+
+var UserSchema = new Schema(
+ {
+ username: { type: String, unique: true, required: true },
+ password: { type: String, required: true },
+ firstname: { type: String, required: true },
+ lastname: { type: String, required: true },
+ email: { type: String, required: false },
+ phone: { type: String, required: false },
+ role: { type: String, default: 'user' },
+ totpEnabled: { type: Boolean, default: false },
+ totpSecret: { type: String, default: '' },
+ enabled: { type: Boolean, default: true },
+ refreshTokens: [
+ { _id: false, sessionId: String, userAgent: String, token: String },
+ ],
+ },
+ { timestamps: true },
+);
+
+var totpConfig = {
+ issuer: 'AuditForge',
+ label: '',
+ algorithm: 'SHA1',
+ digits: 6,
+ period: 30,
+ secret: '',
+};
+
+//check TOTP token
+var checkTotpToken = function (token, secret) {
+ if (!token) throw { fn: 'BadParameters', message: 'TOTP token required' };
+ if (token.length !== 6)
+ throw { fn: 'BadParameters', message: 'Invalid TOTP token length' };
+ if (!secret) throw { fn: 'BadParameters', message: 'TOTP secret required' };
+
+ let newConfig = totpConfig;
+ newConfig.secret = secret;
+ let totp = new OTPAuth.TOTP(newConfig);
+ let delta = totp.validate({
+ token: token,
+ window: 5,
+ });
+ //The token is valid in 2 windows in the past and the future, current window is 0.
+ if (delta === null) {
+ throw { fn: 'Unauthorized', message: 'Wrong TOTP token.' };
+ } else if (delta < -2 || delta > 2) {
+ throw { fn: 'Unauthorized', message: 'TOTP token out of window.' };
+ }
+ return true;
+};
+
+/*
+ *** Statics ***
+ */
+
+// Create user
+UserSchema.statics.create = function (user) {
+ return new Promise((resolve, reject) => {
+ var hash = bcrypt.hashSync(user.password, 10);
+ user.password = hash;
+ new User(user)
+ .save()
+ .then(function () {
+ resolve();
+ })
+ .catch(function (err) {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Get all users
+UserSchema.statics.getAll = function () {
+ return new Promise((resolve, reject) => {
+ var query = this.find();
+ query.select(
+ 'username firstname lastname email phone role totpEnabled enabled',
+ );
+ query
+ .exec()
+ .then(function (rows) {
+ resolve(rows);
+ })
+ .catch(function (err) {
+ reject(err);
+ });
+ });
+};
+
+// Get one user by its username
+UserSchema.statics.getByUsername = function (username) {
+ return new Promise((resolve, reject) => {
+ var query = this.findOne({ username: username });
+ query.select(
+ 'username firstname lastname email phone role totpEnabled enabled',
+ );
+ query
+ .exec()
+ .then(function (row) {
+ if (row) resolve(row);
+ else throw { fn: 'NotFound', message: 'User not found' };
+ })
+ .catch(function (err) {
+ reject(err);
+ });
+ });
+};
+
+// Update user with password verification (for updating my profile)
+UserSchema.statics.updateProfile = function (username, user) {
+ return new Promise((resolve, reject) => {
+ var query = this.findOne({ username: username });
+ var payload = {};
+ query
+ .exec()
+ .then(function (row) {
+ if (!row) throw { fn: 'NotFound', message: 'User not found' };
+ else if (bcrypt.compareSync(user.password, row.password)) {
+ if (user.username) row.username = user.username;
+ if (user.firstname) row.firstname = user.firstname;
+ if (user.lastname) row.lastname = user.lastname;
+ if (!_.isNil(user.email)) row.email = user.email;
+ if (!_.isNil(user.phone)) row.phone = user.phone;
+ if (user.newPassword)
+ row.password = bcrypt.hashSync(user.newPassword, 10);
+ if (typeof user.totpEnabled == 'boolean')
+ row.totpEnabled = user.totpEnabled;
+
+ payload.id = row._id;
+ payload.username = row.username;
+ payload.role = row.role;
+ payload.firstname = row.firstname;
+ payload.lastname = row.lastname;
+ payload.email = row.email;
+ payload.phone = row.phone;
+ payload.roles = auth.acl.getRoles(payload.role);
+
+ return row.save();
+ } else
+ throw { fn: 'Unauthorized', message: 'Current password is invalid' };
+ })
+ .then(function () {
+ var token = jwt.sign(payload, auth.jwtSecret, {
+ expiresIn: '15 minutes',
+ });
+ resolve({ token: `JWT ${token}` });
+ })
+ .catch(function (err) {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Update user (for admin usage)
+UserSchema.statics.updateUser = function (userId, user) {
+ return new Promise((resolve, reject) => {
+ if (user.password) user.password = bcrypt.hashSync(user.password, 10);
+ var query = this.findOneAndUpdate({ _id: userId }, user);
+ query
+ .exec()
+ .then(function (row) {
+ if (row) resolve('User updated successfully');
+ else reject({ fn: 'NotFound', message: 'User not found' });
+ })
+ .catch(function (err) {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Update refreshtoken
+UserSchema.statics.updateRefreshToken = function (refreshToken, userAgent) {
+ return new Promise((resolve, reject) => {
+ var token = '';
+ var newRefreshToken = '';
+ try {
+ var decoded = jwt.verify(refreshToken, auth.jwtRefreshSecret);
+ var userId = decoded.userId;
+ var sessionId = decoded.sessionId;
+ var expiration = decoded.exp;
+ } catch (err) {
+ if (err.name === 'TokenExpiredError')
+ throw { fn: 'Unauthorized', message: 'Expired refreshToken' };
+ else throw { fn: 'Unauthorized', message: 'Invalid refreshToken' };
+ }
+ var query = this.findById(userId);
+ query
+ .exec()
+ .then(row => {
+ if (row && row.enabled !== false) {
+ // Check session exist and sessionId not null (if null then it is a login)
+ if (sessionId !== null) {
+ var sessionExist = row.refreshTokens.findIndex(
+ e => e.sessionId === sessionId && e.token === refreshToken,
+ );
+ if (sessionExist === -1)
+ // Not found
+ throw { fn: 'Unauthorized', message: 'Session not found' };
+ }
+
+ // Generate new token
+ var payload = {};
+ payload.id = row._id;
+ payload.username = row.username;
+ payload.role = row.role;
+ payload.firstname = row.firstname;
+ payload.lastname = row.lastname;
+ payload.email = row.email;
+ payload.phone = row.phone;
+ payload.roles = auth.acl.getRoles(payload.role);
+
+ token = jwt.sign(payload, auth.jwtSecret, {
+ expiresIn: '15 minutes',
+ });
+
+ // Remove expired sessions
+ row.refreshTokens = row.refreshTokens.filter(e => {
+ try {
+ var decoded = jwt.verify(e.token, auth.jwtRefreshSecret);
+ } catch (err) {
+ var decoded = null;
+ }
+ return decoded !== null;
+ });
+ // Update or add new refresh token
+ var foundIndex = row.refreshTokens.findIndex(
+ e => e.sessionId === sessionId,
+ );
+ if (foundIndex === -1) {
+ // Not found
+ sessionId = generateUUID();
+ newRefreshToken = jwt.sign(
+ { sessionId: sessionId, userId: userId },
+ auth.jwtRefreshSecret,
+ { expiresIn: '7 days' },
+ );
+ row.refreshTokens.push({
+ sessionId: sessionId,
+ userAgent: userAgent,
+ token: newRefreshToken,
+ });
+ } else {
+ newRefreshToken = jwt.sign(
+ { sessionId: sessionId, userId: userId, exp: expiration },
+ auth.jwtRefreshSecret,
+ );
+ row.refreshTokens[foundIndex].token = newRefreshToken;
+ }
+ return row.save();
+ } else if (row) {
+ reject({ fn: 'Unauthorized', message: 'Account disabled' });
+ } else reject({ fn: 'NotFound', message: 'Session not found' });
+ })
+ .then(() => {
+ resolve({ token: token, refreshToken: newRefreshToken });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// Remove session
+UserSchema.statics.removeSession = function (userId, sessionId) {
+ return new Promise((resolve, reject) => {
+ var query = this.findById(userId);
+ query
+ .exec()
+ .then(row => {
+ if (row) {
+ row.refreshTokens = row.refreshTokens.filter(
+ e => e.sessionId !== sessionId,
+ );
+ return row.save();
+ } else reject({ fn: 'NotFound', message: 'User not found' });
+ })
+ .then(() => {
+ resolve('Session removed successfully');
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({ fn: 'BadParameters', message: 'Username already exists' });
+ else reject(err);
+ });
+ });
+};
+
+// gen totp QRCode url
+UserSchema.statics.getTotpQrcode = function (username) {
+ return new Promise((resolve, reject) => {
+ let newConfig = totpConfig;
+ newConfig.label = username;
+ const secret = new OTPAuth.Secret({
+ size: 10,
+ }).base32;
+ newConfig.secret = secret;
+
+ let totp = new OTPAuth.TOTP(newConfig);
+ let totpUrl = totp.toString();
+
+ QRCode.toDataURL(totpUrl, function (err, url) {
+ resolve({
+ totpQrCode: url,
+ totpSecret: secret,
+ });
+ });
+ });
+};
+
+// verify TOTP and Setup enabled status and secret code
+UserSchema.statics.setupTotp = function (token, secret, username) {
+ return new Promise((resolve, reject) => {
+ checkTotpToken(token, secret);
+
+ var query = this.findOne({ username: username });
+ query
+ .exec()
+ .then(function (row) {
+ if (!row) throw { errmsg: 'User not found' };
+ else if (row.totpEnabled === true)
+ throw { errmsg: 'TOTP already enabled by this user' };
+ else {
+ row.totpEnabled = true;
+ row.totpSecret = secret;
+ return row.save();
+ }
+ })
+ .then(function () {
+ resolve({ msg: true });
+ })
+ .catch(function (err) {
+ reject(err);
+ });
+ });
+};
+
+// verify TOTP and Cancel enabled status and secret code
+UserSchema.statics.cancelTotp = function (token, username) {
+ return new Promise((resolve, reject) => {
+ var query = this.findOne({ username: username });
+ query
+ .exec()
+ .then(function (row) {
+ if (!row) throw { errmsg: 'User not found' };
+ else if (row.totpEnabled !== true)
+ throw { errmsg: 'TOTP is not enabled yet' };
+ else {
+ checkTotpToken(token, row.totpSecret);
+ row.totpEnabled = false;
+ row.totpSecret = '';
+ return row.save();
+ }
+ })
+ .then(function () {
+ resolve({ msg: 'TOTP is canceled.' });
+ })
+ .catch(function (err) {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+// Authenticate user with username and password, return JWT token
+UserSchema.methods.getToken = function (userAgent) {
+ return new Promise((resolve, reject) => {
+ var user = this;
+ var query = User.findOne({ username: user.username });
+ query
+ .exec()
+ .then(function (row) {
+ if (row && row.enabled === false)
+ throw { fn: 'Unauthorized', message: 'Authentication Failed.' };
+
+ if (row && bcrypt.compareSync(user.password, row.password)) {
+ if (row.totpEnabled && user.totpToken)
+ checkTotpToken(user.totpToken, row.totpSecret);
+ else if (row.totpEnabled)
+ throw { fn: 'BadParameters', message: 'Missing TOTP token' };
+ var refreshToken = jwt.sign(
+ { sessionId: null, userId: row._id },
+ auth.jwtRefreshSecret,
+ );
+ return User.updateRefreshToken(refreshToken, userAgent);
+ } else {
+ if (!row) {
+ // We compare two random strings to generate delay
+ var randomHash =
+ '$2b$10$' +
+ [...Array(53)].map(() => Math.random().toString(36)[2]).join('');
+ bcrypt.compareSync(user.password, randomHash);
+ }
+
+ throw { fn: 'Unauthorized', message: 'Authentication Failed.' };
+ }
+ })
+ .then(row => {
+ resolve({ token: row.token, refreshToken: row.refreshToken });
+ })
+ .catch(function (err) {
+ reject(err);
+ });
+ });
+};
+
+var User = mongoose.model('User', UserSchema);
+module.exports = User;
diff --git a/backend/src/models/vulnerability-category.js b/backend/src/models/vulnerability-category.js
new file mode 100644
index 0000000000000000000000000000000000000000..8da2b2e722fab502e073f44330f95e438ff73029
--- /dev/null
+++ b/backend/src/models/vulnerability-category.js
@@ -0,0 +1,124 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var VulnerabilityCategorySchema = new Schema(
+ {
+ name: { type: String, unique: true },
+ sortValue: { type: String, default: 'cvssScore' },
+ sortOrder: { type: String, enum: ['desc', 'asc'], default: 'desc' },
+ sortAuto: { type: Boolean, default: true },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all vulnerabilityCategorys
+VulnerabilityCategorySchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = VulnerabilityCategory.find();
+ query.select('name sortValue sortOrder sortAuto');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create vulnerabilityCategory
+VulnerabilityCategorySchema.statics.create = vulnerabilityCategory => {
+ return new Promise((resolve, reject) => {
+ var query = new VulnerabilityCategory(vulnerabilityCategory);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Vulnerability Category name already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update vulnerabilityCategory
+VulnerabilityCategorySchema.statics.update = (name, vulnerabilityCategory) => {
+ return new Promise((resolve, reject) => {
+ var query = VulnerabilityCategory.findOneAndUpdate(
+ { name: name },
+ vulnerabilityCategory,
+ );
+ query
+ .exec()
+ .then(row => {
+ if (row) resolve(row);
+ else
+ reject({
+ fn: 'NotFound',
+ message: 'Vulnerability category not found',
+ });
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Vulnerability Category already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update vulnerability Categories
+VulnerabilityCategorySchema.statics.updateAll = vulnCategories => {
+ return new Promise((resolve, reject) => {
+ VulnerabilityCategory.deleteMany()
+ .then(row => {
+ VulnerabilityCategory.insertMany(vulnCategories);
+ })
+ .then(row => {
+ resolve('Vulnerability Categories updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete vulnerabilityCategory
+VulnerabilityCategorySchema.statics.delete = name => {
+ return new Promise((resolve, reject) => {
+ VulnerabilityCategory.deleteOne({ name: name })
+ .then(res => {
+ if (res.deletedCount === 1) resolve('Vulnerability Category deleted');
+ else
+ reject({
+ fn: 'NotFound',
+ message: 'Vulnerability Category not found',
+ });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var VulnerabilityCategory = mongoose.model(
+ 'VulnerabilityCategory',
+ VulnerabilityCategorySchema,
+);
+module.exports = VulnerabilityCategory;
diff --git a/backend/src/models/vulnerability-type.js b/backend/src/models/vulnerability-type.js
new file mode 100644
index 0000000000000000000000000000000000000000..cb0b07b7e215a765c86ff4c9e00a626bac268504
--- /dev/null
+++ b/backend/src/models/vulnerability-type.js
@@ -0,0 +1,96 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var VulnerabilityTypeSchema = new Schema(
+ {
+ name: String,
+ locale: String,
+ },
+ { timestamps: true },
+);
+
+VulnerabilityTypeSchema.index(
+ { name: 1, locale: 1 },
+ { name: 'unique_name_locale', unique: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all vulnerabilityTypes
+VulnerabilityTypeSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = VulnerabilityType.find();
+ query.select('-_id name locale');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create vulnerabilityType
+VulnerabilityTypeSchema.statics.create = vulnerabilityType => {
+ return new Promise((resolve, reject) => {
+ var query = new VulnerabilityType(vulnerabilityType);
+ query
+ .save()
+ .then(row => {
+ resolve(row);
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Vulnerability Type already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Update vulnerability Types
+VulnerabilityTypeSchema.statics.updateAll = vulnerabilityTypes => {
+ return new Promise((resolve, reject) => {
+ VulnerabilityType.deleteMany()
+ .then(row => {
+ VulnerabilityType.insertMany(vulnerabilityTypes);
+ })
+ .then(row => {
+ resolve('Vulnerability Types updated successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete vulnerabilityType
+VulnerabilityTypeSchema.statics.delete = name => {
+ return new Promise((resolve, reject) => {
+ VulnerabilityType.deleteOne({ name: name })
+ .then(res => {
+ if (res.deletedCount === 1) resolve('Vulnerability Type deleted');
+ else
+ reject({ fn: 'NotFound', message: 'Vulnerability Type not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var VulnerabilityType = mongoose.model(
+ 'VulnerabilityType',
+ VulnerabilityTypeSchema,
+);
+module.exports = VulnerabilityType;
diff --git a/backend/src/models/vulnerability-update.js b/backend/src/models/vulnerability-update.js
new file mode 100644
index 0000000000000000000000000000000000000000..987e0c31d55defc87f30dcfea53383ea83547103
--- /dev/null
+++ b/backend/src/models/vulnerability-update.js
@@ -0,0 +1,190 @@
+var mongoose = require('mongoose');
+var _ = require('lodash');
+
+var Schema = mongoose.Schema;
+
+var customField = {
+ _id: false,
+ customField: { type: Schema.Types.ObjectId, ref: 'CustomField' },
+ text: Schema.Types.Mixed,
+};
+
+var VulnerabilityUpdateSchema = new Schema(
+ {
+ vulnerability: {
+ type: Schema.Types.ObjectId,
+ ref: 'Vulnerability',
+ required: true,
+ },
+ creator: { type: Schema.Types.ObjectId, ref: 'User', required: true },
+ cvssv3: String,
+ priority: { type: Number, enum: [1, 2, 3, 4] },
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
+ references: [String],
+ locale: String,
+ title: String,
+ vulnType: String,
+ description: String,
+ observation: String,
+ remediation: String,
+ category: String,
+ customFields: [customField],
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all vulnerabilities
+VulnerabilityUpdateSchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = VulnerabilityUpdate.find();
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get all updates of vulnerability
+VulnerabilityUpdateSchema.statics.getAllByVuln = vulnId => {
+ return new Promise((resolve, reject) => {
+ var query = VulnerabilityUpdate.find({ vulnerability: vulnId });
+ query.populate('creator', '-_id username');
+ query.populate('customFields.customField', 'fieldType label');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create vulnerability
+VulnerabilityUpdateSchema.statics.create = (username, vulnerability) => {
+ return new Promise((resolve, reject) => {
+ var created = true;
+ var User = mongoose.model('User');
+ var creator = '';
+ var Vulnerability = mongoose.model('Vulnerability');
+ var query = User.findOne({ username: username });
+ query
+ .exec()
+ .then(row => {
+ if (row) {
+ creator = row._id;
+ var query = Vulnerability.findOne({
+ 'details.title': vulnerability.title,
+ });
+ return query.exec();
+ } else throw { fn: 'NotFound', message: 'User not found' };
+ })
+ .then(row => {
+ if (row) {
+ if (row.status === 1)
+ throw {
+ fn: 'Forbidden',
+ message: 'Vulnerability not approved yet',
+ };
+ else {
+ // Check if there are any changes from the original vulnerability
+ var detail = row.details.find(
+ d => d.locale === vulnerability.locale,
+ );
+ // console.log(vulnerability.customFields)
+ // console.log(detail.customFields)
+ if (
+ typeof detail !== 'undefined' &&
+ (row.cvssv3 || '').includes(vulnerability.cvssv3) &&
+ vulnerability.priority === (row.priority || null) &&
+ vulnerability.remediationComplexity ===
+ (row.remediationComplexity || null) &&
+ _.isEqual(vulnerability.references, detail.references || []) &&
+ vulnerability.category === (row.category || null) &&
+ vulnerability.vulnType === (detail.vulnType || null) &&
+ vulnerability.description === (detail.description || null) &&
+ vulnerability.observation === (detail.observation || null) &&
+ vulnerability.remediation === (detail.remediation || null) &&
+ vulnerability.customFields.length ===
+ detail.customFields.length &&
+ vulnerability.customFields.every((e, idx) => {
+ return (
+ e.customField._id == detail.customFields[idx].customField &&
+ e.text === detail.customFields[idx].text
+ );
+ })
+ ) {
+ throw {
+ fn: 'BadParameters',
+ message: 'No changes from the original vulnerability',
+ };
+ }
+ vulnerability.vulnerability = row._id;
+ vulnerability.creator = creator;
+ var query = new VulnerabilityUpdate(vulnerability);
+ created = false;
+ return query.save();
+ }
+ } else {
+ var vuln = {};
+ vuln.cvssv3 = vulnerability.cvssv3 || null;
+ vuln.priority = vulnerability.priority || null;
+ vuln.remediationComplexity =
+ vulnerability.remediationComplexity || null;
+ vuln.category = vulnerability.category || null;
+ vuln.creator = creator;
+ var details = {};
+ details.locale = vulnerability.locale || null;
+ details.title = vulnerability.title || null;
+ details.vulnType = vulnerability.vulnType || null;
+ details.description = vulnerability.description || null;
+ details.observation = vulnerability.observation || null;
+ details.remediation = vulnerability.remediation || null;
+ details.references = vulnerability.references || null;
+ details.customFields = vulnerability.customFields || [];
+ vuln.details = [details];
+ var query = new Vulnerability(vuln);
+ return query.save();
+ }
+ })
+ .then(row => {
+ if (created) resolve('Finding created as new Vulnerability');
+ else {
+ var query = Vulnerability.findOneAndUpdate(
+ { 'details.title': vulnerability.title },
+ { status: 2 },
+ );
+ return query.exec();
+ }
+ })
+ .then(row => {
+ resolve('Update proposed for existing vulnerability');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+VulnerabilityUpdateSchema.statics.deleteAllByVuln = async vulnId => {
+ return await VulnerabilityUpdate.deleteMany({ vulnerability: vulnId });
+};
+
+/*
+ *** Methods ***
+ */
+
+var VulnerabilityUpdate = mongoose.model(
+ 'VulnerabilityUpdate',
+ VulnerabilityUpdateSchema,
+);
+module.exports = VulnerabilityUpdate;
diff --git a/backend/src/models/vulnerability.js b/backend/src/models/vulnerability.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d0cd00778d80b8f2ca48f9762e01c55fc913849
--- /dev/null
+++ b/backend/src/models/vulnerability.js
@@ -0,0 +1,272 @@
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+
+var customField = {
+ _id: false,
+ customField: { type: Schema.Types.ObjectId, ref: 'CustomField' },
+ text: Schema.Types.Mixed,
+};
+
+var VulnerabilityDetails = {
+ _id: false,
+ locale: String,
+ // language: String,
+ title: { type: String, unique: true, sparse: true },
+ vulnType: String,
+ description: String,
+ observation: String,
+ remediation: String,
+ cwes: [String],
+ references: [String],
+ customFields: [customField],
+};
+
+var VulnerabilitySchema = new Schema(
+ {
+ cvssv3: String,
+ priority: { type: Number, enum: [1, 2, 3, 4] },
+ remediationComplexity: { type: Number, enum: [1, 2, 3] },
+ details: [VulnerabilityDetails],
+ status: { type: Number, enum: [0, 1, 2], default: 1 }, // 0: validated, 1: created, 2: updated,
+ category: String,
+ creator: { type: Schema.Types.ObjectId, ref: 'User' },
+ },
+ { timestamps: true },
+);
+
+/*
+ *** Statics ***
+ */
+
+// Get all vulnerabilities
+VulnerabilitySchema.statics.getAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Vulnerability.find();
+ query.populate('creator', '-_id username');
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get all vulnerabilities for download
+VulnerabilitySchema.statics.export = () => {
+ return new Promise((resolve, reject) => {
+ var query = Vulnerability.find();
+ query.select(
+ 'details cvssv3 priority remediationComplexity cwes references category -_id',
+ );
+ query
+ .exec()
+ .then(rows => {
+ resolve(rows);
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Create vulnerability
+VulnerabilitySchema.statics.create = vulnerabilities => {
+ return new Promise((resolve, reject) => {
+ Vulnerability.insertMany(vulnerabilities, { ordered: false })
+ .then(rows => {
+ resolve({ created: rows.length, duplicates: 0 });
+ })
+ .catch(err => {
+ if (err.code === 11000) {
+ if (err.result.nInserted === 0)
+ reject({
+ fn: 'BadParameters',
+ message: 'Vulnerability title already exists',
+ });
+ else {
+ var errorMessages = [];
+ err.writeErrors.forEach(e =>
+ errorMessages.push(e.errmsg || 'no errmsg'),
+ );
+ resolve({
+ created: err.result.nInserted,
+ duplicates: errorMessages,
+ });
+ }
+ } else reject(err);
+ });
+ });
+};
+
+// Update vulnerability
+VulnerabilitySchema.statics.update = (vulnerabilityId, vulnerability) => {
+ return new Promise((resolve, reject) => {
+ var VulnerabilityUpdate = mongoose.model('VulnerabilityUpdate');
+ var query = Vulnerability.findByIdAndUpdate(vulnerabilityId, vulnerability);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
+ else {
+ var query = VulnerabilityUpdate.deleteMany({
+ vulnerability: vulnerabilityId,
+ });
+ return query.exec();
+ }
+ })
+ .then(row => {
+ resolve('Vulnerability updated successfully');
+ })
+ .catch(err => {
+ if (err.code === 11000)
+ reject({
+ fn: 'BadParameters',
+ message: 'Vulnerability title already exists',
+ });
+ else reject(err);
+ });
+ });
+};
+
+// Delete all vulnerabilities
+VulnerabilitySchema.statics.deleteAll = () => {
+ return new Promise((resolve, reject) => {
+ var query = Vulnerability.deleteMany();
+ query
+ .exec()
+ .then(() => {
+ resolve('All vulnerabilities deleted successfully');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Delete vulnerability
+VulnerabilitySchema.statics.delete = vulnerabilityId => {
+ return new Promise((resolve, reject) => {
+ var query = Vulnerability.findByIdAndDelete(vulnerabilityId);
+ query
+ .exec()
+ .then(rows => {
+ if (rows) resolve(rows);
+ else reject({ fn: 'NotFound', message: 'Vulnerability not found' });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+// Get vulnerabilities by language
+VulnerabilitySchema.statics.getAllByLanguage = locale => {
+ return new Promise((resolve, reject) => {
+ var query = Vulnerability.find({ 'details.locale': locale });
+ query.select('details cvssv3 priority remediationComplexity category');
+ query
+ .exec()
+ .then(rows => {
+ if (rows) {
+ var result = [];
+ rows.forEach(row => {
+ row.details.forEach(detail => {
+ if (detail.locale === locale && detail.title) {
+ var temp = {};
+ temp.cvssv3 = row.cvssv3;
+ temp.priority = row.priority;
+ temp.remediationComplexity = row.remediationComplexity;
+ temp.category = row.category;
+ temp.detail = detail;
+ temp._id = row._id;
+ result.push(temp);
+ }
+ });
+ });
+ resolve(result);
+ } else
+ reject({
+ fn: 'NotFound',
+ message: 'Locale with existing title not found',
+ });
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+VulnerabilitySchema.statics.Merge = (vulnIdPrime, vulnIdMerge, locale) => {
+ return new Promise((resolve, reject) => {
+ var mergeDetail = null;
+ var mergeVuln = null;
+ var primeVuln = null;
+ var query = Vulnerability.findById(vulnIdMerge);
+ query
+ .exec()
+ .then(row => {
+ if (!row)
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
+ else {
+ mergeVuln = row;
+ mergeDetail = row.details.find(d => d.locale === locale);
+ var query = Vulnerability.findById(vulnIdPrime);
+ return query.exec();
+ }
+ })
+ .then(row => {
+ if (!row)
+ reject({ fn: 'NotFound', message: 'Vulnerability not found' });
+ else {
+ if (row.details.findIndex(d => d.locale === locale && d.title) !== -1)
+ reject({
+ fn: 'BadParameters',
+ message: 'Language already exists in this vulnerability',
+ });
+ else {
+ primeVuln = row;
+ var removeIndex = mergeVuln.details
+ .map(d => d.title)
+ .indexOf(mergeDetail.title);
+ mergeVuln.details.splice(removeIndex, 1);
+ if (mergeVuln.details.length === 0)
+ return Vulnerability.findByIdAndDelete(mergeVuln._id);
+ else return mergeVuln.save();
+ }
+ }
+ })
+ .then(() => {
+ var detail = {};
+ detail.locale = mergeDetail.locale;
+ detail.title = mergeDetail.title;
+ if (mergeDetail.vulnType) detail.vulnType = mergeDetail.vulnType;
+ if (mergeDetail.description)
+ detail.description = mergeDetail.description;
+ if (mergeDetail.observation)
+ detail.observation = mergeDetail.observation;
+ if (mergeDetail.remediation)
+ detail.remediation = mergeDetail.remediation;
+ if (mergeDetail.customFields)
+ detail.customFields = mergeDetail.customFields;
+ primeVuln.details.push(detail);
+ return primeVuln.save();
+ })
+ .then(() => {
+ resolve('Vulnerability merge successfull');
+ })
+ .catch(err => {
+ reject(err);
+ });
+ });
+};
+
+/*
+ *** Methods ***
+ */
+
+var Vulnerability = mongoose.model('Vulnerability', VulnerabilitySchema);
+module.exports = Vulnerability;
diff --git a/backend/src/routes/audit.js b/backend/src/routes/audit.js
new file mode 100644
index 0000000000000000000000000000000000000000..626a34e319545ec0a09e38b79e252f32d69f8785
--- /dev/null
+++ b/backend/src/routes/audit.js
@@ -0,0 +1,1168 @@
+module.exports = function (app, io) {
+ var Response = require('../lib/httpResponse');
+ var Audit = require('mongoose').model('Audit');
+ var acl = require('../lib/auth').acl;
+ var reportGenerator = require('../lib/report-generator');
+ var _ = require('lodash');
+ var utils = require('../lib/utils');
+ var Settings = require('mongoose').model('Settings');
+
+ /* ### AUDITS LIST ### */
+
+ // Get audits list of user (all for admin) with regex filter on findings
+ app.get('/api/audits', acl.hasPermission('audits:read'), function (req, res) {
+ var getUsersRoom = function (room) {
+ return utils.getSockets(io, room).map(s => s.username);
+ };
+ var filters = {};
+ if (req.query.findingTitle)
+ filters['findings.title'] = new RegExp(
+ utils.escapeRegex(req.query.findingTitle),
+ 'i',
+ );
+ if (req.query.type && req.query.type === 'default')
+ filters.$or = [{ type: 'default' }, { type: { $exists: false } }];
+ if (req.query.type && ['multi', 'retest'].includes(req.query.type))
+ filters.type = req.query.type;
+
+ Audit.getAudits(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.decodedToken.id,
+ filters,
+ )
+ .then(msg => {
+ var result = [];
+ msg.forEach(audit => {
+ var a = {};
+ a._id = audit._id;
+ a.name = audit.name;
+ a.language = audit.language;
+ a.auditType = audit.auditType;
+ a.creator = audit.creator;
+ a.collaborators = audit.collaborators;
+ a.company = audit.company;
+ a.createdAt = audit.createdAt;
+ a.ext =
+ !!audit.template && !!audit.template.ext
+ ? audit.template.ext
+ : 'Template error';
+ a.reviewers = audit.reviewers;
+ a.approvals = audit.approvals;
+ a.state = audit.state;
+ a.type = audit.type;
+ a.parentId = audit.parentId;
+ if (acl.isAllowed(req.decodedToken.role, 'audits:users-connected')) {
+ a.connected = getUsersRoom(audit._id.toString());
+ }
+ result.push(a);
+ });
+ Response.Ok(res, result);
+ })
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Create audit (default or multi) with name, auditType, language provided
+ // parentId can be set only if type is default
+ app.post(
+ '/api/audits',
+ acl.hasPermission('audits:create'),
+ function (req, res) {
+ if (!req.body.name || !req.body.language || !req.body.auditType) {
+ Response.BadParameters(
+ res,
+ 'Missing some required parameters: name, language, auditType',
+ );
+ return;
+ }
+
+ if (!utils.validFilename(req.body.language)) {
+ Response.BadParameters(res, 'Invalid characters for language');
+ return;
+ }
+
+ var audit = {};
+ // Required params
+ audit.name = req.body.name;
+ audit.language = req.body.language;
+ audit.auditType = req.body.auditType;
+ audit.type = 'default';
+
+ // Optional params
+ if (req.body.type && req.body.type === 'multi')
+ audit.type = req.body.type;
+ if (audit.type === 'default' && req.body.parentId)
+ audit.parentId = req.body.parentId;
+
+ Audit.create(audit, req.decodedToken.id)
+ .then(inserted =>
+ Response.Created(res, {
+ message: 'Audit created successfully',
+ audit: inserted,
+ }),
+ )
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get audits children
+ app.get(
+ '/api/audits/:auditId/children',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ var getUsersRoom = function (room) {
+ return utils.getSockets(io, room).map(s => s.username);
+ };
+ Audit.getAuditChildren(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => {
+ var result = [];
+ msg.forEach(audit => {
+ var a = {};
+ a._id = audit._id;
+ a.name = audit.name;
+ a.auditType = audit.auditType;
+ a.approvals = audit.approvals;
+ a.state = audit.state;
+ if (
+ acl.isAllowed(req.decodedToken.role, 'audits:users-connected')
+ ) {
+ a.connected = getUsersRoom(audit._id.toString());
+ }
+ result.push(a);
+ });
+ Response.Ok(res, result);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get audit retest with auditId
+ app.get(
+ '/api/audits/:auditId/retest',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getRetest(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create audit retest with auditId
+ app.post(
+ '/api/audits/:auditId/retest',
+ acl.hasPermission('audits:create'),
+ function (req, res) {
+ if (!req.body.auditType) {
+ Response.BadParameters(
+ res,
+ 'Missing some required parameters: auditType',
+ );
+ return;
+ }
+ Audit.createRetest(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.body.auditType,
+ )
+ .then(inserted =>
+ Response.Created(res, {
+ message: 'Audit Retest created successfully',
+ audit: inserted,
+ }),
+ )
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete audit if creator or admin
+ app.delete(
+ '/api/audits/:auditId',
+ acl.hasPermission('audits:delete'),
+ function (req, res) {
+ Audit.delete(
+ acl.isAllowed(req.decodedToken.role, 'audits:delete-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ### AUDITS EDIT ### */
+
+ // Get Audit with ID
+ app.get(
+ '/api/audits/:auditId',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get audit general information
+ app.get(
+ '/api/audits/:auditId/general',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update audit general information
+ app.put(
+ '/api/audits/:auditId/general',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var update = {};
+
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+
+ if (req.body.reviewers) {
+ if (req.body.reviewers.some(element => !element._id)) {
+ Response.BadParameters(res, 'One or more reviewer is missing an _id');
+ return;
+ }
+
+ // Is the new reviewer the creator of the audit?
+ if (
+ req.body.reviewers.some(element => element._id === audit.creator._id)
+ ) {
+ Response.BadParameters(
+ res,
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
+ );
+ return;
+ }
+
+ // Is the new reviewer one of the new collaborators that will override current collaborators?
+ if (req.body.collaborators) {
+ req.body.reviewers.forEach(reviewer => {
+ if (
+ req.body.collaborators.some(
+ element => !element._id || element._id === reviewer._id,
+ )
+ ) {
+ Response.BadParameters(
+ res,
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
+ );
+ return;
+ }
+ });
+ }
+
+ // If no new collaborators are being set, is the new reviewer one of the current collaborators?
+ else if (audit.collaborators) {
+ req.body.reviewers.forEach(reviewer => {
+ if (
+ audit.collaborators.some(element => element._id === reviewer._id)
+ ) {
+ Response.BadParameters(
+ res,
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
+ );
+ return;
+ }
+ });
+ }
+ }
+
+ if (req.body.collaborators) {
+ if (req.body.collaborators.some(element => !element._id)) {
+ Response.BadParameters(
+ res,
+ 'One or more collaborator is missing an _id',
+ );
+ return;
+ }
+
+ // Are the new collaborators part of the current reviewers?
+ req.body.collaborators.forEach(collaborator => {
+ if (
+ audit.reviewers.some(element => element._id === collaborator._id)
+ ) {
+ Response.BadParameters(
+ res,
+ 'A user cannot simultaneously be a reviewer and a collaborator/creator',
+ );
+ return;
+ }
+ });
+
+ // If the new collaborator already gave a review, remove said review, accept collaborator
+ if (audit.approvals) {
+ var newApprovals = audit.approvals.filter(
+ approval =>
+ !req.body.collaborators.some(
+ collaborator => approval.toString() === collaborator._id,
+ ),
+ );
+ update.approvals = newApprovals;
+ }
+ }
+
+ // Optional parameters
+ if (req.body.name) update.name = req.body.name;
+ if (req.body.date) update.date = req.body.date;
+ if (req.body.date_start) update.date_start = req.body.date_start;
+ if (req.body.date_end) update.date_end = req.body.date_end;
+ if (req.body.client !== undefined) update.client = req.body.client;
+ if (req.body.company !== undefined) {
+ update.company = {};
+ if (req.body.company && req.body.company._id)
+ update.company._id = req.body.company._id;
+ else if (req.body.company && req.body.company.name)
+ update.company.name = req.body.company.name;
+ else update.company = null;
+ }
+ if (req.body.collaborators) update.collaborators = req.body.collaborators;
+ if (req.body.reviewers) update.reviewers = req.body.reviewers;
+ if (req.body.language && utils.validFilename(req.body.language))
+ update.language = req.body.language;
+ if (req.body.scope && typeof (req.body.scope === 'array')) {
+ update.scope = req.body.scope.map(item => {
+ return { name: item };
+ });
+ }
+ if (req.body.template) update.template = req.body.template;
+ if (req.body.customFields) update.customFields = req.body.customFields;
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ )
+ update.approvals = [];
+
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ update,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get audit network information
+ app.get(
+ '/api/audits/:auditId/network',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getNetwork(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update audit network information
+ app.put(
+ '/api/audits/:auditId/network',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+
+ var update = {};
+ // Optional parameters
+ if (req.body.scope) update.scope = req.body.scope;
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ )
+ update.approvals = [];
+
+ Audit.updateNetwork(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ update,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // POST to export an encrypted PDF.
+ app.post(
+ '/api/audits/:auditId/generate/pdf',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(async audit => {
+ if (
+ ['ppt', 'pptx', 'doc', 'docx', 'docm'].includes(audit.template.ext)
+ ) {
+ let reportPdf;
+ if (req.body.password) {
+ reportPdf = await reportGenerator.generateEncryptedPdf(
+ audit,
+ req.body.password,
+ );
+ } else {
+ Response.BadParameters(res, 'No password included');
+ }
+
+ if (reportPdf) {
+ res.setHeader(
+ 'Content-Disposition',
+ `attachment; filename=${audit.name}.pdf`,
+ );
+ res.setHeader('Content-Type', 'application/pdf');
+ res.send(reportPdf);
+ } else {
+ console.error('Error generating PDF');
+ Response.Internal(res, 'Error generating PDF');
+ }
+ } else {
+ Response.BadParameters(
+ res,
+ 'Template not in a Microsoft Word/Powerpoint format',
+ );
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ if (err.code === 'ENOENT')
+ Response.BadParameters(res, 'Template File not found');
+ else Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Generate report as PDF
+ app.get(
+ '/api/audits/:auditId/generate/pdf',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(async audit => {
+ if (
+ ['ppt', 'pptx', 'doc', 'docx', 'docm'].find(
+ ext => ext === audit.template.ext,
+ )
+ ) {
+ var reportPdf = await reportGenerator.generatePdf(audit);
+ Response.SendFile(res, `${audit.name}.pdf`, reportPdf);
+ } else {
+ Response.BadParameters(
+ res,
+ 'Template not in a Microsoft Word/Powerpoint format',
+ );
+ }
+ })
+ .catch(err => {
+ console.log(err);
+ if (err.code === 'ENOENT')
+ Response.BadParameters(res, 'Template File not found');
+ else Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Generate Report as csv
+ app.get(
+ '/api/audits/:auditId/generate/csv',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(async audit => {
+ var reportCsv = await reportGenerator.generateCsv(audit);
+ Response.SendFile(res, `${audit.name}.csv`, reportCsv);
+ })
+ .catch(err => {
+ console.log(err);
+ Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Generate Report as json
+ app.get(
+ '/api/audits/:auditId/generate/json',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(async audit => {
+ Response.SendFile(res, `${audit.name}.json`, audit);
+ })
+ .catch(err => {
+ console.log(err);
+ Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Add finding to audit
+ app.post(
+ '/api/audits/:auditId/findings',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ if (!req.body.title) {
+ Response.BadParameters(res, 'Missing some required parameters: title');
+ return;
+ }
+
+ var finding = {};
+ // Required parameters
+ finding.title = req.body.title;
+
+ // Optional parameters
+ if (req.body.vulnType) finding.vulnType = req.body.vulnType;
+ if (req.body.description) finding.description = req.body.description;
+ if (req.body.observation) finding.observation = req.body.observation;
+ if (req.body.remediation) finding.remediation = req.body.remediation;
+ if (req.body.remediationComplexity)
+ finding.remediationComplexity = req.body.remediationComplexity;
+ if (req.body.priority) finding.priority = req.body.priority;
+ if (req.body.references) finding.references = req.body.references;
+ if (req.body.cwes) finding.cwes = req.body.cwes;
+ if (req.body.cvssv3) finding.cvssv3 = req.body.cvssv3;
+ if (req.body.poc) finding.poc = req.body.poc;
+ if (req.body.scope) finding.scope = req.body.scope;
+ if (req.body.status !== undefined) finding.status = req.body.status;
+ if (req.body.category) finding.category = req.body.category;
+ if (req.body.customFields) finding.customFields = req.body.customFields;
+
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ ) {
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ { approvals: [] },
+ );
+ }
+
+ Audit.createFinding(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ finding,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get finding of audit
+ app.get(
+ '/api/audits/:auditId/findings/:findingId',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getFinding(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.params.findingId,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update finding of audit
+ app.put(
+ '/api/audits/:auditId/findings/:findingId',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+
+ var finding = {};
+ // Optional parameters
+ if (req.body.title) finding.title = req.body.title;
+ if (req.body.vulnType) finding.vulnType = req.body.vulnType;
+ if (!_.isNil(req.body.description))
+ finding.description = req.body.description;
+ if (!_.isNil(req.body.observation))
+ finding.observation = req.body.observation;
+ if (!_.isNil(req.body.remediation))
+ finding.remediation = req.body.remediation;
+ if (req.body.remediationComplexity)
+ finding.remediationComplexity = req.body.remediationComplexity;
+ if (req.body.priority) finding.priority = req.body.priority;
+ if (req.body.references) finding.references = req.body.references;
+ if (req.body.cwes) finding.cwes = req.body.cwes;
+ if (req.body.cvssv3) finding.cvssv3 = req.body.cvssv3;
+ if (!_.isNil(req.body.poc)) finding.poc = req.body.poc;
+ if (!_.isNil(req.body.scope)) finding.scope = req.body.scope;
+ if (req.body.status !== undefined) finding.status = req.body.status;
+ if (req.body.category) finding.category = req.body.category;
+ if (req.body.customFields) finding.customFields = req.body.customFields;
+ if (req.body.retestDescription)
+ finding.retestDescription = req.body.retestDescription;
+ if (req.body.retestStatus) finding.retestStatus = req.body.retestStatus;
+
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ ) {
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ { approvals: [] },
+ );
+ }
+
+ Audit.updateFinding(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.params.findingId,
+ finding,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete finding of audit
+ app.delete(
+ '/api/audits/:auditId/findings/:findingId',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ Audit.deleteFinding(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.params.findingId,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get section of audit
+ app.get(
+ '/api/audits/:auditId/sections/:sectionId',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getSection(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.params.sectionId,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update section of audit
+ app.put(
+ '/api/audits/:auditId/sections/:sectionId',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ if (typeof req.body.customFields === 'undefined') {
+ Response.BadParameters(
+ res,
+ 'Missing some required parameters: customFields',
+ );
+ return;
+ }
+ var section = {};
+ // Mandatory parameters
+ section.customFields = req.body.customFields;
+
+ // For retrocompatibility with old section.text usage
+ if (req.body.text) section.text = req.body.text;
+
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ ) {
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ { approvals: [] },
+ );
+ }
+
+ Audit.updateSection(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.params.sectionId,
+ section,
+ )
+ .then(msg => {
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Generate Report for specific audit
+ app.get(
+ '/api/audits/:auditId/generate',
+ acl.hasPermission('audits:read'),
+ function (req, res) {
+ Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(async audit => {
+ var settings = await Settings.getAll();
+
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.public.mandatoryReview &&
+ audit.state !== 'APPROVED'
+ ) {
+ Response.Forbidden(
+ res,
+ 'Audit was not approved therefore cannot be exported.',
+ );
+ return;
+ }
+
+ if (!audit.template)
+ throw { fn: 'BadParameters', message: 'Template not defined' };
+
+ var reportDoc = await reportGenerator.generateDoc(audit);
+ Response.SendFile(
+ res,
+ `${audit.name.replace(/[\\\/:*?"<>|]/g, '')}.${audit.template.ext || 'docx'}`,
+ reportDoc,
+ );
+ })
+ .catch(err => {
+ if (err.code === 'ENOENT')
+ Response.BadParameters(res, 'Template File not found');
+ else Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Update sort options of an audit
+ app.put(
+ '/api/audits/:auditId/sortfindings',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ var update = {};
+ // Optional parameters
+ if (req.body.sortFindings) update.sortFindings = req.body.sortFindings;
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ )
+ update.approvals = [];
+
+ Audit.updateSortFindings(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ update,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update finding position (oldIndex -> newIndex)
+ app.put(
+ '/api/audits/:auditId/movefinding',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ if (
+ typeof req.body.oldIndex === 'undefined' ||
+ typeof req.body.newIndex === 'undefined'
+ ) {
+ Response.BadParameters(
+ res,
+ 'Missing some required parameters: oldIndex, newIndex',
+ );
+ return;
+ }
+
+ var move = {};
+ // Required parameters
+ move.oldIndex = req.body.oldIndex;
+ move.newIndex = req.body.newIndex;
+
+ if (
+ settings.reviews.enabled &&
+ settings.reviews.private.removeApprovalsUponUpdate
+ ) {
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ { approvals: [] },
+ );
+ }
+
+ Audit.moveFindingPosition(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ move,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Give or remove a reviewer's approval to an audit
+ app.put(
+ '/api/audits/:auditId/toggleApproval',
+ acl.hasPermission('audits:review'),
+ async function (req, res) {
+ const settings = await Settings.getAll();
+
+ if (!settings.reviews.enabled) {
+ Response.Forbidden(res, 'Audit reviews are not enabled.');
+ return;
+ }
+
+ Audit.findById(req.params.auditId)
+ .then(audit => {
+ if (audit.state !== 'REVIEW' && audit.state !== 'APPROVED') {
+ Response.Forbidden(
+ res,
+ 'The audit is not approvable in the current state.',
+ );
+ return;
+ }
+
+ var hasApprovedBefore = false;
+ var newApprovalsArray = [];
+ if (audit.approvals) {
+ audit.approvals.forEach(approval => {
+ if (approval._id.toString() === req.decodedToken.id) {
+ hasApprovedBefore = true;
+ } else {
+ newApprovalsArray.push(approval);
+ }
+ });
+ }
+
+ if (!hasApprovedBefore) {
+ newApprovalsArray.push({
+ _id: req.decodedToken.id,
+ role: req.decodedToken.role,
+ username: req.decodedToken.username,
+ firstname: req.decodedToken.firstname,
+ lastname: req.decodedToken.lastname,
+ });
+ }
+
+ var update = { approvals: newApprovalsArray };
+ Audit.updateApprovals(
+ acl.isAllowed(req.decodedToken.role, 'audits:review-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ update,
+ )
+ .then(() => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, 'Approval updated successfully.');
+ })
+ .catch(err => {
+ Response.Internal(res, err);
+ });
+ })
+ .catch(err => {
+ Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Sets the audit state to EDIT or REVIEW
+ app.put(
+ '/api/audits/:auditId/updateReadyForReview',
+ acl.hasPermission('audits:update'),
+ async function (req, res) {
+ const settings = await Settings.getAll();
+
+ if (!settings.reviews.enabled) {
+ Response.Forbidden(res, 'Audit reviews are not enabled.');
+ return;
+ }
+
+ var update = {};
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+
+ if (audit.state !== 'EDIT' && audit.state !== 'REVIEW') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the proper state for this action.',
+ );
+ return;
+ }
+
+ if (
+ req.body.state != undefined &&
+ (req.body.state === 'EDIT' || req.body.state === 'REVIEW')
+ )
+ update.state = req.body.state;
+
+ if (update.state === 'EDIT') {
+ var newApprovalsArray = [];
+ if (audit.approvals) {
+ audit.approvals.forEach(approval => {
+ if (approval._id.toString() !== req.decodedToken.id) {
+ newApprovalsArray.push(approval);
+ }
+ });
+ update.approvals = newApprovalsArray;
+ }
+ }
+
+ Audit.updateGeneral(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ update,
+ )
+ .then(msg => {
+ io.to(req.params.auditId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update parentId of Audit
+ app.put(
+ '/api/audits/:auditId/updateParent',
+ acl.hasPermission('audits:create'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.body.parentId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && audit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ if (!req.body.parentId) {
+ Response.BadParameters(
+ res,
+ 'Missing some required parameters: parentId',
+ );
+ return;
+ }
+ Audit.updateParent(
+ acl.isAllowed(req.decodedToken.role, 'audits:update-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ req.body.parentId,
+ )
+ .then(msg => {
+ io.to(req.body.parentId).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete parentId of Audit
+ app.delete(
+ '/api/audits/:auditId/deleteParent',
+ acl.hasPermission('audits:delete'),
+ async function (req, res) {
+ var settings = await Settings.getAll();
+ var audit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ );
+ var parentAudit = await Audit.getAudit(
+ acl.isAllowed(req.decodedToken.role, 'audits:read-all'),
+ audit.parentId,
+ req.decodedToken.id,
+ );
+ if (settings.reviews.enabled && parentAudit.state !== 'EDIT') {
+ Response.Forbidden(
+ res,
+ 'The audit is not in the EDIT state and therefore cannot be edited.',
+ );
+ return;
+ }
+ Audit.deleteParent(
+ acl.isAllowed(req.decodedToken.role, 'audits:delete-all'),
+ req.params.auditId,
+ req.decodedToken.id,
+ )
+ .then(msg => {
+ if (msg.parentId) io.to(msg.parentId.toString()).emit('updateAudit');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/check-cwe-update.js b/backend/src/routes/check-cwe-update.js
new file mode 100644
index 0000000000000000000000000000000000000000..a579de5ea82c9f2398d7c25f9a5bdc98f8cf0a95
--- /dev/null
+++ b/backend/src/routes/check-cwe-update.js
@@ -0,0 +1,60 @@
+module.exports = function (app) {
+ const Response = require('../lib/httpResponse.js');
+ const acl = require('../lib/auth').acl;
+ const networkError = new Error(
+ 'Error checking CWE model update: Network response was not ok',
+ );
+ const timeoutError = new Error(
+ 'Error checking CWE mode update: Request timed out',
+ );
+ const cweConfig = require('../config/config-cwe.json')['cwe-container'];
+ const TIMEOUT_MS = cweConfig.check_timeout_ms || 30000;
+
+ app.get(
+ '/api/check-cwe-update',
+ acl.hasPermission('check-update:all'),
+ async function (req, res) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
+
+ try {
+ //TODO: Change workaround to a proper solution for self-signed certificates
+ if (!cweConfig.host || !cweConfig.port) {
+ return Response.BadRequest(
+ res,
+ new Error('Configuración del servicio incompleta'),
+ );
+ }
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ const response = await fetch(
+ `https://${cweConfig.host}:${cweConfig.port}/${cweConfig.endpoints.check_update_endpoint}`,
+ {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ signal: controller.signal,
+ },
+ );
+ clearTimeout(timeout);
+
+ if (!response.ok) {
+ const errorBody = await response.text();
+ throw new Error(
+ `Error del servidor CWE (${response.status}): ${errorBody}`,
+ );
+ }
+
+ const data = await response.json();
+ res.json(data);
+ } catch (error) {
+ console.error('Error en check-cwe-update:', {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ });
+ error.name === 'AbortError'
+ ? Response.Internal(res, { ...timeoutError, details: error.message })
+ : Response.Internal(res, { ...networkError, details: error.message });
+ }
+ },
+ );
+};
diff --git a/backend/src/routes/client.js b/backend/src/routes/client.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc124589f54f1c3b9b2208e5546ce9e318709761
--- /dev/null
+++ b/backend/src/routes/client.js
@@ -0,0 +1,80 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var Client = require('mongoose').model('Client');
+ var acl = require('../lib/auth').acl;
+
+ // Get clients list
+ app.get(
+ '/api/clients',
+ acl.hasPermission('clients:read'),
+ function (req, res) {
+ Client.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create client
+ app.post(
+ '/api/clients',
+ acl.hasPermission('clients:create'),
+ function (req, res) {
+ if (!req.body.email) {
+ Response.BadParameters(res, 'Required parameters: email');
+ return;
+ }
+
+ var client = {};
+ // Required parameters
+ client.email = req.body.email;
+
+ // Optional parameters
+ if (req.body.lastname) client.lastname = req.body.lastname;
+ if (req.body.firstname) client.firstname = req.body.firstname;
+ if (req.body.phone) client.phone = req.body.phone;
+ if (req.body.cell) client.cell = req.body.cell;
+ if (req.body.title) client.title = req.body.title;
+ var company = null;
+ if (req.body.company && req.body.company.name)
+ company = req.body.company.name;
+
+ Client.create(client, company)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update client
+ app.put(
+ '/api/clients/:id',
+ acl.hasPermission('clients:update'),
+ function (req, res) {
+ var client = {};
+ // Optional parameters
+ if (req.body.email) client.email = req.body.email;
+ client.lastname = req.body.lastname || null;
+ client.firstname = req.body.firstname || null;
+ client.phone = req.body.phone || null;
+ client.cell = req.body.cell || null;
+ client.title = req.body.title || null;
+ var company = null;
+ if (req.body.company && req.body.company.name)
+ company = req.body.company.name;
+
+ Client.update(req.params.id, client, company)
+ .then(msg => Response.Ok(res, 'Client updated successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete client
+ app.delete(
+ '/api/clients/:id',
+ acl.hasPermission('clients:delete'),
+ function (req, res) {
+ Client.delete(req.params.id)
+ .then(msg => Response.Ok(res, 'Client deleted successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/company.js b/backend/src/routes/company.js
new file mode 100644
index 0000000000000000000000000000000000000000..f16083ec45b28d41ffcbc1d62b45900cdcb6420e
--- /dev/null
+++ b/backend/src/routes/company.js
@@ -0,0 +1,68 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var Company = require('mongoose').model('Company');
+ var acl = require('../lib/auth').acl;
+
+ // Get companies list
+ app.get(
+ '/api/companies',
+ acl.hasPermission('companies:read'),
+ function (req, res) {
+ Company.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create company
+ app.post(
+ '/api/companies',
+ acl.hasPermission('companies:create'),
+ function (req, res) {
+ if (!req.body.name) {
+ Response.BadParameters(res, 'Required paramters: name');
+ return;
+ }
+
+ var company = {};
+ // Required parameters
+ company.name = req.body.name;
+
+ // Optional parameters
+ if (req.body.shortName) company.shortName = req.body.shortName;
+ if (req.body.logo) company.logo = req.body.logo;
+
+ Company.create(company)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update company
+ app.put(
+ '/api/companies/:id',
+ acl.hasPermission('companies:update'),
+ function (req, res) {
+ var company = {};
+ // Optional parameters
+ if (req.body.name) company.name = req.body.name;
+ if (req.body.shortName) company.shortName = req.body.shortName;
+ if (req.body.logo) company.logo = req.body.logo;
+
+ Company.update(req.params.id, company)
+ .then(msg => Response.Ok(res, 'Company updated successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete company
+ app.delete(
+ '/api/companies/:id',
+ acl.hasPermission('companies:delete'),
+ function (req, res) {
+ Company.delete(req.params.id)
+ .then(msg => Response.Ok(res, 'Company deleted successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/cvss.js b/backend/src/routes/cvss.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff03444b0cc5c42dcce0524eab89b8c32fe7c817
--- /dev/null
+++ b/backend/src/routes/cvss.js
@@ -0,0 +1,61 @@
+module.exports = function (app) {
+ const Response = require('../lib/httpResponse.js');
+ const acl = require('../lib/auth').acl;
+ const cweConfig = require('../config/config-cwe.json')['cwe-container'];
+ const errorClassify = new Error('Error classifying vulnerability');
+ const networkError = new Error('Network response was not ok');
+ const timeoutError = new Error('Request timed out');
+ const TIMEOUT_MS = 47000; // 47 segundos (temporal)
+
+ // Get CVSS string from description
+ app.post(
+ '/api/cvss',
+ acl.hasPermission('classify_cvss:all'),
+ async function (req, res) {
+ if (
+ !req.body.vuln ||
+ typeof req.body.vuln !== 'string' ||
+ req.body.vuln.trim() === ''
+ ) {
+ Response.BadParameters(res, 'Required parameters: description');
+ return;
+ }
+
+ const vuln = {
+ vuln: req.body.vuln.trim(),
+ };
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
+
+ try {
+ //TODO: Change workaround to a proper solution for self-signed certificates
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ const response = await fetch(
+ `https://${cweConfig.host}:${cweConfig.port}/cvss`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(vuln),
+ signal: controller.signal,
+ },
+ );
+
+ if (!response.ok) {
+ throw networkError;
+ }
+
+ const data = await response.json();
+ res.json(data);
+ } catch (error) {
+ console.error(error);
+ error.name === 'AbortError'
+ ? Response.Internal(res, timeoutError)
+ : Response.Internal(res, errorClassify);
+ } finally {
+ clearTimeout(timeout);
+ }
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
+ },
+ );
+};
diff --git a/backend/src/routes/cwe.js b/backend/src/routes/cwe.js
new file mode 100644
index 0000000000000000000000000000000000000000..1180914b0396236206565d6b8d1e023a83fb119b
--- /dev/null
+++ b/backend/src/routes/cwe.js
@@ -0,0 +1,60 @@
+module.exports = function (app) {
+ const Response = require('../lib/httpResponse.js');
+ const acl = require('../lib/auth').acl;
+ const cweConfig = require('../config/config-cwe.json')['cwe-container'];
+ const errorClassify = new Error('Error classifying vulnerability');
+ const networkError = new Error('Network response was not ok');
+ const timeoutError = new Error('Request timed out');
+ const TIMEOUT_MS = 5000; // 5 segundos
+
+ // Get CWE classification from description
+ app.post(
+ '/api/classify',
+ acl.hasPermission('classify:all'),
+ async function (req, res) {
+ if (
+ !req.body.vuln ||
+ typeof req.body.vuln !== 'string' ||
+ req.body.vuln.trim() === ''
+ ) {
+ Response.BadParameters(res, 'Required parameters: description');
+ return;
+ }
+
+ const vuln = {
+ vuln: req.body.vuln.trim(),
+ };
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
+
+ try {
+ //TODO: Change workaround to a proper solution for self-signed certificates
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ const response = await fetch(
+ `https://${cweConfig.host}:${cweConfig.port}/classify`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(vuln),
+ signal: controller.signal,
+ },
+ );
+ clearTimeout(timeout);
+
+ if (!response.ok) {
+ throw networkError;
+ }
+
+ const data = await response.json();
+ res.json(data);
+ } catch (error) {
+ console.error(error);
+ error.name === 'AbortError'
+ ? Response.Internal(res, timeoutError)
+ : Response.Internal(res, errorClassify);
+ }
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
+ },
+ );
+};
diff --git a/backend/src/routes/data.js b/backend/src/routes/data.js
new file mode 100644
index 0000000000000000000000000000000000000000..712184fb4e629c00105b1bbe86ef19877ac72fff
--- /dev/null
+++ b/backend/src/routes/data.js
@@ -0,0 +1,666 @@
+const { isArray } = require('lodash');
+
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var acl = require('../lib/auth').acl;
+ var utils = require('../lib/utils');
+ var Language = require('mongoose').model('Language');
+ var AuditType = require('mongoose').model('AuditType');
+ var VulnerabilityType = require('mongoose').model('VulnerabilityType');
+ var VulnerabilityCategory = require('mongoose').model(
+ 'VulnerabilityCategory',
+ );
+ var CustomSection = require('mongoose').model('CustomSection');
+ var CustomField = require('mongoose').model('CustomField');
+
+ var _ = require('lodash');
+
+ /* ===== ROLES ===== */
+
+ // Get Roles list
+ app.get(
+ '/api/data/roles',
+ acl.hasPermission('roles:read'),
+ function (req, res) {
+ try {
+ var result = Object.keys(acl.roles);
+ Response.Ok(res, result);
+ } catch (error) {
+ Response.Internal(res, error);
+ }
+ },
+ );
+
+ /* ===== LANGUAGES ===== */
+
+ // Get languages list
+ app.get(
+ '/api/data/languages',
+ acl.hasPermission('languages:read'),
+ function (req, res) {
+ Language.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create language
+ app.post(
+ '/api/data/languages',
+ acl.hasPermission('languages:create'),
+ function (req, res) {
+ if (!req.body.locale || !req.body.language) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: locale, language',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(req.body.language) ||
+ !utils.validFilename(req.body.locale)
+ ) {
+ Response.BadParameters(
+ res,
+ "language and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var language = {};
+ language.locale = req.body.locale;
+ language.language = req.body.language;
+
+ Language.create(language)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete Language
+ app.delete(
+ '/api/data/languages/:locale(*)',
+ acl.hasPermission('languages:delete'),
+ function (req, res) {
+ Language.delete(req.params.locale)
+ .then(msg => {
+ Response.Ok(res, 'Language deleted successfully');
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update Languages
+ app.put(
+ '/api/data/languages',
+ acl.hasPermission('languages:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var language = req.body[i];
+ if (!language.locale || !language.language) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: locale, language',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(language.language) ||
+ !utils.validFilename(language.locale)
+ ) {
+ Response.BadParameters(
+ res,
+ "language and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var languages = [];
+ req.body.forEach(e => {
+ languages.push({ language: e.language, locale: e.locale });
+ });
+
+ Language.updateAll(languages)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ===== AUDIT TYPES ===== */
+
+ // Get audit types list
+ app.get(
+ '/api/data/audit-types',
+ acl.hasPermission('audit-types:read'),
+ function (req, res) {
+ AuditType.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create audit type
+ app.post(
+ '/api/data/audit-types',
+ acl.hasPermission('audit-types:create'),
+ function (req, res) {
+ if (!req.body.name || !req.body.templates) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, templates',
+ );
+ return;
+ }
+ if (!utils.validFilename(req.body.name)) {
+ Response.BadParameters(
+ res,
+ "name and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var auditType = {};
+ auditType.stage = 'default';
+ // Required parameters
+ auditType.name = req.body.name;
+ auditType.templates = req.body.templates;
+
+ // Optional parameters
+ if (req.body.sections) auditType.sections = req.body.sections;
+ if (req.body.hidden) auditType.hidden = req.body.hidden;
+ if (
+ req.body.stage &&
+ (req.body.stage === 'multi' || req.body.stage === 'retest')
+ )
+ auditType.stage = req.body.stage;
+
+ // Fix hidden sections for multi and retest audits
+ if (auditType.stage === 'multi' || auditType.stage === 'retest')
+ auditType.hidden = ['network'];
+
+ AuditType.create(auditType)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete audit type
+ app.delete(
+ '/api/data/audit-types/:name(*)',
+ acl.hasPermission('audit-types:delete'),
+ function (req, res) {
+ AuditType.delete(req.params.name)
+ .then(msg => {
+ Response.Ok(res, 'Audit type deleted successfully');
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update Audit Types
+ app.put(
+ '/api/data/audit-types',
+ acl.hasPermission('audit-types:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var auditType = req.body[i];
+ if (!auditType.name || !auditType.templates) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, templates',
+ );
+ return;
+ }
+ if (!utils.validFilename(auditType.name)) {
+ Response.BadParameters(
+ res,
+ "name and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var auditTypes = [];
+ req.body.forEach(e => {
+ // Fix hidden sections for multi and retest audits
+ if (e.stage === 'multi' || e.stage === 'retest')
+ auditTypes.push({
+ name: e.name,
+ templates: e.templates,
+ sections: e.sections,
+ hidden: ['network'],
+ stage: e.stage,
+ });
+ else
+ auditTypes.push({
+ name: e.name,
+ templates: e.templates,
+ sections: e.sections,
+ hidden: e.hidden,
+ stage: 'default',
+ });
+ });
+
+ AuditType.updateAll(auditTypes)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ===== VULNERABILITY TYPES ===== */
+
+ // Get vulnerability types list
+ app.get(
+ '/api/data/vulnerability-types',
+ acl.hasPermission('vulnerability-types:read'),
+ function (req, res) {
+ VulnerabilityType.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create vulnerability type
+ app.post(
+ '/api/data/vulnerability-types',
+ acl.hasPermission('vulnerability-types:create'),
+ function (req, res) {
+ if (!req.body.name || !req.body.locale) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, locale',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(req.body.name) ||
+ !utils.validFilename(req.body.locale)
+ ) {
+ Response.BadParameters(
+ res,
+ "name and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var vulnType = {};
+ vulnType.name = req.body.name;
+ vulnType.locale = req.body.locale;
+ VulnerabilityType.create(vulnType)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete vulnerability type
+ app.delete(
+ '/api/data/vulnerability-types/:name(*)',
+ acl.hasPermission('vulnerability-types:delete'),
+ function (req, res) {
+ VulnerabilityType.delete(req.params.name)
+ .then(msg => {
+ Response.Ok(res, 'Vulnerability type deleted successfully');
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update Vulnerability Types
+ app.put(
+ '/api/data/vulnerability-types',
+ acl.hasPermission('vulnerability-types:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var vulnType = req.body[i];
+ if (!vulnType.name || !vulnType.locale) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, locale',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(vulnType.name) ||
+ !utils.validFilename(vulnType.locale)
+ ) {
+ Response.BadParameters(
+ res,
+ "name and locale value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var vulnTypes = [];
+ req.body.forEach(e => {
+ vulnTypes.push({ name: e.name, locale: e.locale });
+ });
+
+ VulnerabilityType.updateAll(vulnTypes)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ===== VULNERABILITY CATEGORY ===== */
+
+ // Get vulnerability category list
+ app.get(
+ '/api/data/vulnerability-categories',
+ acl.hasPermission('vulnerability-categories:read'),
+ function (req, res) {
+ VulnerabilityCategory.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create vulnerability category
+ app.post(
+ '/api/data/vulnerability-categories',
+ acl.hasPermission('vulnerability-categories:create'),
+ function (req, res) {
+ if (!req.body.name) {
+ Response.BadParameters(res, 'Missing required parameters: name');
+ return;
+ }
+ if (!utils.validFilename(req.body.name)) {
+ Response.BadParameters(
+ res,
+ "name value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var vulnCat = {};
+ // Required parameters
+ vulnCat.name = req.body.name;
+
+ // Optional parameters
+ if (!_.isNil(req.body.sortValue)) vulnCat.sortValue = req.body.sortValue;
+ if (!_.isNil(req.body.sortOrder)) vulnCat.sortOrder = req.body.sortOrder;
+ if (!_.isNil(req.body.sortAuto)) vulnCat.sortAuto = req.body.sortAuto;
+
+ VulnerabilityCategory.create(vulnCat)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update Vulnerability Category
+ app.put(
+ '/api/data/vulnerability-categories',
+ acl.hasPermission('vulnerability-categories:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var vulnCat = req.body[i];
+ if (!vulnCat.name) {
+ Response.BadParameters(res, 'Missing required parameters: name');
+ return;
+ }
+ if (!utils.validFilename(vulnCat.name)) {
+ Response.BadParameters(
+ res,
+ "name value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var vulnCategories = [];
+ req.body.forEach(e => {
+ // Required parameters
+ var tmpCat = { name: e.name };
+
+ // Optional parameters
+ if (!_.isNil(e.sortValue)) tmpCat.sortValue = e.sortValue;
+ if (!_.isNil(e.sortOrder)) tmpCat.sortOrder = e.sortOrder;
+ if (!_.isNil(e.sortAuto)) tmpCat.sortAuto = e.sortAuto;
+
+ vulnCategories.push(tmpCat);
+ });
+
+ VulnerabilityCategory.updateAll(vulnCategories)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete vulnerability category
+ app.delete(
+ '/api/data/vulnerability-categories/:name(*)',
+ acl.hasPermission('vulnerability-categories:delete'),
+ function (req, res) {
+ VulnerabilityCategory.delete(req.params.name)
+ .then(msg => {
+ Response.Ok(res, 'Vulnerability category deleted successfully');
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ===== SECTIONS ===== */
+
+ // Get sections list
+ app.get(
+ '/api/data/sections',
+ acl.hasPermission('sections:read'),
+ function (req, res) {
+ CustomSection.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create section
+ app.post(
+ '/api/data/sections',
+ acl.hasPermission('sections:create'),
+ function (req, res) {
+ if (!req.body.field || !req.body.name) {
+ Response.BadParameters(res, 'Missing required parameters: field, name');
+ return;
+ }
+ if (
+ !utils.validFilename(req.body.field) ||
+ !utils.validFilename(req.body.name)
+ ) {
+ Response.BadParameters(
+ res,
+ "name and field value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var section = {};
+ section.field = req.body.field;
+ section.name = req.body.name;
+ section.locale = req.body.locale;
+ // Optional parameters
+ if (req.body.text) section.text = req.body.text;
+ if (req.body.icon) section.icon = req.body.icon;
+
+ CustomSection.create(section)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete section
+ app.delete(
+ '/api/data/sections/:field/:locale(*)',
+ acl.hasPermission('sections:delete'),
+ function (req, res) {
+ CustomSection.delete(req.params.field, req.params.locale)
+ .then(msg => {
+ Response.Ok(res, 'Section deleted successfully');
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update sections
+ app.put(
+ '/api/data/sections',
+ acl.hasPermission('sections:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var section = req.body[i];
+ if (!section.name || !section.field) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, field',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(section.name) ||
+ !utils.validFilename(section.field)
+ ) {
+ Response.BadParameters(
+ res,
+ "name and field value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var sections = [];
+ req.body.forEach(e => {
+ sections.push({ name: e.name, field: e.field, icon: e.icon || '' });
+ });
+
+ CustomSection.updateAll(sections)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ /* ===== CUSTOM FIELDS ===== */
+
+ // Get custom fields
+ app.get(
+ '/api/data/custom-fields',
+ acl.hasPermission('custom-fields:read'),
+ function (req, res) {
+ CustomField.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create custom field
+ app.post(
+ '/api/data/custom-fields',
+ acl.hasPermission('custom-fields:create'),
+ function (req, res) {
+ if (
+ (!req.body.fieldType || !req.body.label || !req.body.display) &&
+ req.body.fieldType !== 'space'
+ ) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: fieldType, label, display',
+ );
+ return;
+ }
+ if (
+ (!utils.validFilename(req.body.fieldType) ||
+ !utils.validFilename(req.body.label)) &&
+ req.body.fieldType !== 'space'
+ ) {
+ Response.BadParameters(
+ res,
+ "name and field value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+
+ var customField = {};
+ customField.fieldType = req.body.fieldType;
+ customField.label = req.body.label;
+ customField.display = req.body.display;
+ if (req.body.displaySub) customField.displaySub = req.body.displaySub;
+ if (req.body.size) customField.size = req.body.size;
+ if (req.body.offset) customField.offset = req.body.offset;
+ if (
+ typeof req.body.required === 'boolean' &&
+ req.body.fieldType !== 'space'
+ )
+ customField.required = req.body.required;
+ if (req.body.description) customField.description = req.body.description;
+ if (req.body.text) customField.text = req.body.text;
+ if (req.body.options) customField.options = req.body.options;
+ if (typeof req.body.position === 'number')
+ customField.position = req.body.position;
+
+ CustomField.create(customField)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update custom fields
+ app.put(
+ '/api/data/custom-fields',
+ acl.hasPermission('custom-fields:update'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var customField = req.body[i];
+ if (
+ (!customField.label || !customField._id || !customField.display) &&
+ customField.fieldType !== 'space'
+ ) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: _id, label, display',
+ );
+ return;
+ }
+ if (
+ !utils.validFilename(
+ customField.label || !utils.validFilename(customField.fieldType),
+ ) &&
+ customField.fieldType !== 'space'
+ ) {
+ Response.BadParameters(
+ res,
+ "label and fieldType value must match /^[p{Letter}p{Mark}0-9 []'()_-]+$/iu",
+ );
+ return;
+ }
+ }
+
+ var customFields = [];
+ req.body.forEach(e => {
+ var field = { _id: e._id, label: e.label, display: e.display };
+ if (typeof e.size === 'number') field.size = e.size;
+ if (typeof e.offset === 'number') field.offset = e.offset;
+ if (typeof e.required === 'boolean') field.required = e.required;
+ if (!_.isNil(e.description)) field.description = e.description;
+ if (!_.isNil(e.text)) field.text = e.text;
+ if (isArray(e.options)) field.options = e.options;
+ if (typeof e.position === 'number') field.position = e.position;
+ customFields.push(field);
+ });
+
+ CustomField.updateAll(customFields)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete custom field
+ app.delete(
+ '/api/data/custom-fields/:fieldId',
+ acl.hasPermission('custom-fields:delete'),
+ function (req, res) {
+ CustomField.delete(req.params.fieldId)
+ .then(msg => {
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/image.js b/backend/src/routes/image.js
new file mode 100644
index 0000000000000000000000000000000000000000..3b28b23d54dd059568fd5aff2e0f5c3d56a95820
--- /dev/null
+++ b/backend/src/routes/image.js
@@ -0,0 +1,78 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var Image = require('mongoose').model('Image');
+ var acl = require('../lib/auth').acl;
+
+ // Get image
+ app.get(
+ '/api/images/:imageId',
+ acl.hasPermission('images:read'),
+ function (req, res) {
+ Image.getOne(req.params.imageId)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create image
+ app.post(
+ '/api/images/',
+ acl.hasPermission('images:create'),
+ function (req, res) {
+ if (!req.body.value) {
+ Response.BadParameters(res, 'Missing required parameters: value');
+ return;
+ }
+
+ // Type validation
+ if (typeof req.body.value !== 'string') {
+ Response.BadParameters(res, 'value parameter must be a String');
+ return;
+ }
+
+ var image = {};
+ // Required parameters
+ image.value = req.body.value;
+
+ // Optional parameters
+ if (req.body.name) image.name = req.body.name;
+ if (req.body.auditId) image.auditId = req.body.auditId;
+
+ Image.create(image)
+ .then(data => Response.Created(res, data))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete image
+ app.delete(
+ '/api/images/:imageId',
+ acl.hasPermission('images:delete'),
+ function (req, res) {
+ Image.delete(req.params.imageId)
+ .then(data => {
+ Response.Ok(res, 'Image deleted successfully');
+ })
+ .catch(err => {
+ Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Download image file
+ app.get(
+ '/api/images/download/:imageId',
+ acl.hasPermission('images:read'),
+ function (req, res) {
+ Image.getOne(req.params.imageId)
+ .then(data => {
+ var imgBase64 = data.value.split(',')[1];
+ var img = Buffer.from(imgBase64, 'base64');
+ Response.SendImage(res, img);
+ })
+ .catch(err => {
+ Response.Internal(res, err);
+ });
+ },
+ );
+};
diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js
new file mode 100644
index 0000000000000000000000000000000000000000..145293487d929a5c2e95a9d61795150d5278327f
--- /dev/null
+++ b/backend/src/routes/settings.js
@@ -0,0 +1,55 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var acl = require('../lib/auth').acl;
+ var Settings = require('mongoose').model('Settings');
+
+ app.get(
+ '/api/settings',
+ acl.hasPermission('settings:read'),
+ function (req, res) {
+ Settings.getAll()
+ .then(settings => Response.Ok(res, settings))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ app.get(
+ '/api/settings/public',
+ acl.hasPermission('settings:read-public'),
+ function (req, res) {
+ Settings.getPublic()
+ .then(settings => Response.Ok(res, settings))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ app.put(
+ '/api/settings',
+ acl.hasPermission('settings:update'),
+ function (req, res) {
+ Settings.update(req.body)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ app.put(
+ '/api/settings/revert',
+ acl.hasPermission('settings:update'),
+ function (req, res) {
+ Settings.restoreDefaults()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ app.get(
+ '/api/settings/export',
+ acl.hasPermission('settings:read'),
+ function (req, res) {
+ Settings.getAll()
+ .then(settings => Response.SendFile(res, 'app-settings.json', settings))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/template.js b/backend/src/routes/template.js
new file mode 100644
index 0000000000000000000000000000000000000000..4d103c1df8bd789e5a5c08788c60834aa5dd2680
--- /dev/null
+++ b/backend/src/routes/template.js
@@ -0,0 +1,154 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var Template = require('mongoose').model('Template');
+ var acl = require('../lib/auth').acl;
+ var utils = require('../lib/utils');
+ var fs = require('fs');
+
+ // Get templates list
+ app.get(
+ '/api/templates',
+ acl.hasPermission('templates:read'),
+ function (req, res) {
+ Template.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create template
+ app.post(
+ '/api/templates',
+ acl.hasPermission('templates:create'),
+ function (req, res) {
+ if (!req.body.name || !req.body.file || !req.body.ext) {
+ Response.BadParameters(
+ res,
+ 'Missing required parameters: name, ext, file',
+ );
+ return;
+ }
+
+ if (
+ !utils.validFilename(req.body.name) ||
+ !utils.validFilename(req.body.ext)
+ ) {
+ Response.BadParameters(res, 'Bad name or ext format');
+ return;
+ }
+
+ var template = {};
+ // Required parameters
+ template.name = req.body.name;
+ template.ext = req.body.ext;
+
+ Template.create(template)
+ .then(data => {
+ var fileBuffer = Buffer.from(req.body.file, 'base64');
+ fs.writeFileSync(
+ `${__basedir}/../report-templates/${template.name}.${template.ext}`,
+ fileBuffer,
+ );
+ Response.Created(res, data);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update template
+ app.put(
+ '/api/templates/:templateId',
+ acl.hasPermission('templates:update'),
+ function (req, res) {
+ if (req.body.name && !utils.validFilename(req.body.name)) {
+ Response.BadParameters(res, 'Bad name format');
+ return;
+ }
+
+ var template = {};
+ // Optional parameters
+ if (req.body.name) template.name = req.body.name;
+ if (req.body.file && req.body.ext) template.ext = req.body.ext;
+
+ Template.update(req.params.templateId, template)
+ .then(data => {
+ // Update file only
+ if (!req.body.name && req.body.file && req.body.ext) {
+ var fileBuffer = Buffer.from(req.body.file, 'base64');
+ try {
+ fs.unlinkSync(
+ `${__basedir}/../report-templates/${data.name}.${data.ext || 'docx'}`,
+ );
+ } catch {}
+ fs.writeFileSync(
+ `${__basedir}/../report-templates/${data.name}.${req.body.ext}`,
+ fileBuffer,
+ );
+ }
+ // Update name only
+ else if (req.body.name && !req.body.file) {
+ fs.renameSync(
+ `${__basedir}/../report-templates/${data.name}.${data.ext || 'docx'}`,
+ `${__basedir}/../report-templates/${req.body.name}.${data.ext || 'docx'}`,
+ );
+ }
+ // Update both name and file
+ else if (req.body.name && req.body.file && req.body.ext) {
+ var fileBuffer = Buffer.from(req.body.file, 'base64');
+ try {
+ fs.unlinkSync(
+ `${__basedir}/../report-templates/${data.name}.${data.ext || 'docx'}`,
+ );
+ } catch {}
+ fs.writeFileSync(
+ `${__basedir}/../report-templates/${req.body.name}.${req.body.ext}`,
+ fileBuffer,
+ );
+ }
+ Response.Ok(res, 'Template updated successfully');
+ })
+ .catch(err => {
+ if (err.code && err.code === 'ENOENT')
+ Response.NotFound(res, 'Template File was not Found');
+ else Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Delete template
+ app.delete(
+ '/api/templates/:templateId',
+ acl.hasPermission('templates:delete'),
+ function (req, res) {
+ Template.delete(req.params.templateId)
+ .then(data => {
+ fs.unlinkSync(
+ `${__basedir}/../report-templates/${data.name}.${data.ext || 'docx'}`,
+ );
+ Response.Ok(res, 'Template deleted successfully');
+ })
+ .catch(err => {
+ if (err.code && err.code === 'ENOENT')
+ Response.Ok(
+ res,
+ 'Template File not found but deleted successfully in database',
+ );
+ else Response.Internal(res, err);
+ });
+ },
+ );
+
+ // Download template file
+ app.get(
+ '/api/templates/download/:templateId',
+ acl.hasPermission('templates:read'),
+ function (req, res) {
+ Template.getOne(req.params.templateId)
+ .then(data => {
+ var file = `${__basedir}/../report-templates/${data.name}.${data.ext || 'docx'}`;
+ res.download(file, `${data.name}.${data.ext}`);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/update-cwe-model.js b/backend/src/routes/update-cwe-model.js
new file mode 100644
index 0000000000000000000000000000000000000000..267834e09484b0baa25003c35546064899bd4d40
--- /dev/null
+++ b/backend/src/routes/update-cwe-model.js
@@ -0,0 +1,51 @@
+module.exports = function (app) {
+ const Response = require('../lib/httpResponse.js');
+ const acl = require('../lib/auth').acl;
+ const networkError = new Error(
+ 'Error updating CWE model: Network response was not ok',
+ );
+ const timeoutError = new Error('Error updating CWE model: Request timed out');
+ const cweConfig = require('../config/config-cwe.json')['cwe-container'];
+ const TIMEOUT_MS = cweConfig.update_timeout_ms || 120000;
+
+ app.post(
+ '/api/update-cwe-model',
+ acl.hasPermission('update-model:all'),
+ async function (req, res) {
+ const controller = new AbortController();
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
+
+ try {
+ //TODO: Change workaround to a proper solution for self-signed certificates
+ if (!cweConfig.host || !cweConfig.port) {
+ return Response.BadRequest(
+ res,
+ new Error('Configuración del servicio CWE incompleta'),
+ );
+ }
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ const response = await fetch(
+ `https://${cweConfig.host}:${cweConfig.port}/${cweConfig.endpoints.update_cwe_endpoint}`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ signal: controller.signal,
+ },
+ );
+ clearTimeout(timeout);
+
+ if (!response.ok) {
+ throw networkError;
+ }
+
+ const data = await response.json();
+ res.json(data);
+ } catch (error) {
+ console.error(error);
+ error.name === 'AbortError'
+ ? Response.Internal(res, timeoutError)
+ : Response.Internal(res, networkError);
+ }
+ },
+ );
+};
diff --git a/backend/src/routes/user.js b/backend/src/routes/user.js
new file mode 100644
index 0000000000000000000000000000000000000000..db59bbb67e473cf4b704c0c4ab875bf2a9c5cb7d
--- /dev/null
+++ b/backend/src/routes/user.js
@@ -0,0 +1,403 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var User = require('mongoose').model('User');
+ var acl = require('../lib/auth').acl;
+ var jwtRefreshSecret = require('../lib/auth').jwtRefreshSecret;
+ var jwt = require('jsonwebtoken');
+ var _ = require('lodash');
+ var passwordpolicy = require('../lib/passwordpolicy');
+
+ // Check token validity
+ app.get(
+ '/api/users/checktoken',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ Response.Ok(res, req.cookies['token']);
+ },
+ );
+
+ // Refresh token
+ app.get('/api/users/refreshtoken', function (req, res) {
+ var userAgent = req.headers['user-agent'];
+ var token = req.cookies['refreshToken'];
+
+ User.updateRefreshToken(token, userAgent)
+ .then(msg => {
+ res.cookie('token', `JWT ${msg.token}`, {
+ secure: true,
+ httpOnly: true,
+ });
+ res.cookie('refreshToken', msg.refreshToken, {
+ secure: true,
+ httpOnly: true,
+ path: '/api/users/refreshtoken',
+ });
+ Response.Ok(res, msg);
+ })
+ .catch(err => {
+ if (err.fn === 'Unauthorized') {
+ res.clearCookie('token');
+ res.clearCookie('refreshToken');
+ }
+ Response.Internal(res, err);
+ });
+ });
+
+ // Remove token cookie
+ app.delete('/api/users/refreshtoken', function (req, res) {
+ var token = req.cookies['refreshToken'];
+ try {
+ var decoded = jwt.verify(token, jwtRefreshSecret);
+ } catch (err) {
+ res.clearCookie('token');
+ res.clearCookie('refreshToken');
+ if (err.name === 'TokenExpiredError')
+ Response.Unauthorized(res, 'Expired refreshToken');
+ else Response.Unauthorized(res, 'Invalid refreshToken');
+ return;
+ }
+ User.removeSession(decoded.userId, decoded.sessionId)
+ .then(msg => {
+ res.clearCookie('token');
+ res.clearCookie('refreshToken');
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Authenticate user -> return JWT token
+ app.post('/api/users/token', function (req, res) {
+ if (!req.body.password || !req.body.username) {
+ Response.BadParameters(res, 'Required parameters: username, password');
+ return;
+ }
+
+ // Validate types
+ if (
+ typeof req.body.password !== 'string' ||
+ typeof req.body.username !== 'string' ||
+ (req.body.totpToken && typeof req.body.totpToken !== 'string')
+ ) {
+ Response.BadParameters(res, 'Parameters must be of type String');
+ return;
+ }
+
+ var user = new User();
+ //Required params
+ user.username = req.body.username;
+ user.password = req.body.password;
+
+ //Optional params
+ if (req.body.totpToken) user.totpToken = req.body.totpToken;
+
+ user
+ .getToken(req.headers['user-agent'])
+ .then(msg => {
+ res.cookie('token', `JWT ${msg.token}`, {
+ secure: true,
+ httpOnly: true,
+ sameSite: 'None',
+ });
+ res.cookie('refreshToken', msg.refreshToken, {
+ secure: true,
+ httpOnly: true,
+ path: '/api/users/refreshtoken',
+ sameSite: 'None',
+ });
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Check if there are any existing users for creating first user
+ app.get('/api/users/init', function (req, res) {
+ User.getAll()
+ .then(msg => Response.Ok(res, msg.length === 0))
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Get all users
+ app.get('/api/users', acl.hasPermission('users:read'), function (req, res) {
+ User.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Get all reviewers
+ app.get(
+ '/api/users/reviewers',
+ acl.hasPermission('users:read'),
+ function (req, res) {
+ User.getAll()
+ .then(users => {
+ var reviewers = [];
+ users.forEach(user => {
+ if (
+ acl.isAllowed(user.role, 'audits:review') ||
+ acl.isAllowed(user.role, 'audits:review-all')
+ ) {
+ reviewers.push(user);
+ }
+ });
+ Response.Ok(res, reviewers);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get user self
+ app.get(
+ '/api/users/me',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ User.getByUsername(req.decodedToken.username)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ //get TOTP Qrcode URL
+ app.get(
+ '/api/users/totp',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ User.getTotpQrcode(req.decodedToken.username)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ //setup TOTP
+ app.post(
+ '/api/users/totp',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ if (!req.body.totpToken || !req.body.totpSecret) {
+ Response.BadParameters(res, 'Missing some required parameters');
+ return;
+ }
+
+ User.setupTotp(
+ req.body.totpToken,
+ req.body.totpSecret,
+ req.decodedToken.username,
+ )
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ //cancel TOTP
+ app.delete(
+ '/api/users/totp',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ if (!req.body.totpToken) {
+ Response.BadParameters(res, 'Missing some required parameters');
+ return;
+ }
+
+ User.cancelTotp(req.body.totpToken, req.decodedToken.username)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get user by username
+ app.get(
+ '/api/users/:username',
+ acl.hasPermission('users:read'),
+ function (req, res) {
+ User.getByUsername(req.params.username)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create user
+ app.post(
+ '/api/users',
+ acl.hasPermission('users:create'),
+ function (req, res) {
+ if (
+ !req.body.username ||
+ !req.body.password ||
+ !req.body.firstname ||
+ !req.body.lastname
+ ) {
+ Response.BadParameters(res, 'Missing some required parameters');
+ return;
+ }
+ if (passwordpolicy.strongPassword(req.body.password) !== true) {
+ Response.BadParameters(
+ res,
+ 'Password does not match the password policy',
+ );
+ return;
+ }
+
+ var user = {};
+ //Required params
+ user.username = req.body.username;
+ user.password = req.body.password;
+ user.firstname = req.body.firstname;
+ user.lastname = req.body.lastname;
+
+ //Optionals params
+ user.role = req.body.role || 'user';
+ if (req.body.email) user.email = req.body.email;
+ if (req.body.phone) user.phone = req.body.phone;
+
+ User.create(user)
+ .then(msg => Response.Created(res, 'User created successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create First User
+ app.post('/api/users/init', function (req, res) {
+ if (
+ !req.body.username ||
+ !req.body.password ||
+ !req.body.firstname ||
+ !req.body.lastname
+ ) {
+ Response.BadParameters(res, 'Missing some required parameters');
+ return;
+ }
+ if (passwordpolicy.strongPassword(req.body.password) !== true) {
+ Response.BadParameters(
+ res,
+ 'Password does not match the password policy',
+ );
+ return;
+ }
+ var user = {};
+ //Required params
+ user.username = req.body.username;
+ user.password = req.body.password;
+ user.firstname = req.body.firstname;
+ user.lastname = req.body.lastname;
+ user.role = 'admin';
+
+ User.getAll()
+ .then(users => {
+ if (users.length === 0)
+ User.create(user)
+ .then(msg => {
+ var newUser = new User();
+ //Required params
+ newUser.username = req.body.username;
+ newUser.password = req.body.password;
+
+ newUser
+ .getToken(req.headers['user-agent'])
+ .then(msg => {
+ res.cookie('token', `JWT ${msg.token}`, {
+ secure: true,
+ httpOnly: true,
+ });
+ res.cookie('refreshToken', msg.refreshToken, {
+ secure: true,
+ httpOnly: true,
+ path: '/api/users/refreshtoken',
+ });
+ Response.Created(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ })
+ .catch(err => Response.Internal(res, err));
+ else Response.Forbidden(res, 'Already Initialized');
+ })
+ .catch(err => Response.Internal(res, err));
+ });
+
+ // Update my profile
+ app.put(
+ '/api/users/me',
+ acl.hasPermission('validtoken'),
+ function (req, res) {
+ if (
+ !req.body.currentPassword ||
+ (req.body.newPassword && !req.body.confirmPassword) ||
+ (req.body.confirmPassword && !req.body.newPassword)
+ ) {
+ Response.BadParameters(res, 'Missing some required parameters');
+ return;
+ }
+ if (
+ req.body.newPassword &&
+ passwordpolicy.strongPassword(req.body.newPassword) !== true
+ ) {
+ Response.BadParameters(
+ res,
+ 'New Password does not match the password policy',
+ );
+ return;
+ }
+ if (
+ req.body.newPassword &&
+ req.body.confirmPassword &&
+ req.body.newPassword !== req.body.confirmPassword
+ ) {
+ Response.BadParameters(res, 'New password validation failed');
+ return;
+ }
+
+ var user = {};
+ // Required params
+ user.password = req.body.currentPassword;
+
+ // Optionals params
+ if (req.body.username) user.username = req.body.username;
+ if (req.body.newPassword) user.newPassword = req.body.newPassword;
+ if (req.body.firstname) user.firstname = req.body.firstname;
+ if (req.body.lastname) user.lastname = req.body.lastname;
+ if (!_.isNil(req.body.email)) user.email = req.body.email;
+ if (!_.isNil(req.body.phone)) user.phone = req.body.phone;
+
+ User.updateProfile(req.decodedToken.username, user)
+ .then(msg => {
+ res.cookie('token', msg.token, { secure: true, httpOnly: true });
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update any user (admin only)
+ app.put(
+ '/api/users/:id',
+ acl.hasPermission('users:update'),
+ function (req, res) {
+ if (
+ req.body.password &&
+ !passwordpolicy.strongPassword(req.body.password)
+ ) {
+ Response.BadParameters(
+ res,
+ 'New Password does not match the password policy',
+ );
+ return;
+ }
+ var user = {};
+
+ // Optionals params
+ if (req.body.username) user.username = req.body.username;
+ if (req.body.password) user.password = req.body.password;
+ if (req.body.firstname) user.firstname = req.body.firstname;
+ if (req.body.lastname) user.lastname = req.body.lastname;
+ if (!_.isNil(req.body.email)) user.email = req.body.email;
+ if (!_.isNil(req.body.phone)) user.phone = req.body.phone;
+ if (req.body.role) user.role = req.body.role;
+ if (typeof req.body.totpEnabled === 'boolean')
+ user.totpEnabled = req.body.totpEnabled;
+ if (typeof req.body.enabled === 'boolean')
+ user.enabled = req.body.enabled;
+
+ User.updateUser(req.params.id, user)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/routes/vulnerability.js b/backend/src/routes/vulnerability.js
new file mode 100644
index 0000000000000000000000000000000000000000..288d87593572b0716c71b6482b34eed154bbdaa6
--- /dev/null
+++ b/backend/src/routes/vulnerability.js
@@ -0,0 +1,256 @@
+module.exports = function (app) {
+ var Response = require('../lib/httpResponse.js');
+ var acl = require('../lib/auth').acl;
+ var Vulnerability = require('mongoose').model('Vulnerability');
+ var VulnerabilityType = require('mongoose').model('VulnerabilityType');
+ var VulnerabilityCategory = require('mongoose').model(
+ 'VulnerabilityCategory',
+ );
+ var VulnerabilityUpdate = require('mongoose').model('VulnerabilityUpdate');
+
+ // Get vulnerabilities list
+ app.get(
+ '/api/vulnerabilities',
+ acl.hasPermission('vulnerabilities:read'),
+ function (req, res) {
+ Vulnerability.getAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get vulnerabilities for export
+ app.get(
+ '/api/vulnerabilities/export',
+ acl.hasPermission('vulnerabilities:read'),
+ function (req, res) {
+ Vulnerability.export()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create vulnerabilities (array of vulnerabilities)
+ app.post(
+ '/api/vulnerabilities',
+ acl.hasPermission('vulnerabilities:create'),
+ function (req, res) {
+ for (var i = 0; i < req.body.length; i++) {
+ var vuln = req.body[i];
+ if (!vuln.details) {
+ Response.BadParameters(
+ res,
+ 'Required parameters: details.locale, details.title',
+ );
+ return;
+ }
+ var index = vuln.details.findIndex(
+ obj =>
+ obj.locale && obj.locale !== '' && obj.title && obj.title !== '',
+ );
+ if (index < 0) {
+ Response.BadParameters(
+ res,
+ 'Required parameters: details.locale, details.title',
+ );
+ return;
+ }
+ }
+
+ var vulnerabilities = [];
+ for (var i = 0; i < req.body.length; i++) {
+ var vuln = {};
+ vuln.cvssv3 = req.body[i].cvssv3 || null;
+ if (req.body[i].priority) vuln.priority = req.body[i].priority;
+ if (req.body[i].remediationComplexity)
+ vuln.remediationComplexity = req.body[i].remediationComplexity;
+ if (req.body[i].category) {
+ vuln.category = req.body[i].category;
+ VulnerabilityCategory.create({ name: vuln.category }).catch(e => {});
+ }
+ vuln.details = [];
+ req.body[i].details.forEach(d => {
+ if (!d.title || !d.locale)
+ // Array of details may contain entries without title or locale but we don't want to save them
+ return;
+ var details = {};
+ if (d.locale) details.locale = d.locale;
+ if (d.title) details.title = d.title;
+ if (d.vulnType) {
+ details.vulnType = d.vulnType;
+ VulnerabilityType.create({
+ locale: d.locale,
+ name: d.vulnType,
+ }).catch(e => {});
+ }
+ if (d.description) details.description = d.description;
+ if (d.observation) details.observation = d.observation;
+ if (d.remediation) details.remediation = d.remediation;
+ if (d.references) details.references = d.references;
+ if (d.cwes) details.cwes = d.cwes;
+ if (d.customFields) details.customFields = d.customFields;
+ vuln.details.push(details);
+ });
+ vuln.status = 0;
+ vulnerabilities.push(vuln);
+ }
+ Vulnerability.create(vulnerabilities)
+ .then(msg => Response.Created(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Update vulnerability
+ app.put(
+ '/api/vulnerabilities/:vulnerabilityId',
+ acl.hasPermission('vulnerabilities:update'),
+ function (req, res) {
+ if (!req.body.details) {
+ Response.BadParameters(
+ res,
+ 'Required parameters: details.locale, details.title',
+ );
+ return;
+ }
+ var index = req.body.details.findIndex(
+ obj => obj.locale && obj.locale !== '' && obj.title && obj.title !== '',
+ );
+ if (index < 0) {
+ Response.BadParameters(
+ res,
+ 'Required parameters: details.locale, details.title',
+ );
+ return;
+ }
+
+ var vuln = {};
+ if (req.body.cvssv3) vuln.cvssv3 = req.body.cvssv3;
+ if (req.body.priority) vuln.priority = req.body.priority;
+ if (req.body.remediationComplexity)
+ vuln.remediationComplexity = req.body.remediationComplexity;
+ vuln.category = req.body.category || null;
+ vuln.details = [];
+ req.body.details.forEach(d => {
+ if (!d.title || !d.locale) return;
+ var details = {};
+ if (d.locale) details.locale = d.locale;
+ if (d.title) details.title = d.title;
+ if (d.vulnType) details.vulnType = d.vulnType;
+ if (d.description) details.description = d.description;
+ if (d.observation) details.observation = d.observation;
+ if (d.remediation) details.remediation = d.remediation;
+ if (d.cwes) details.cwes = d.cwes;
+ if (d.references) details.references = d.references;
+ if (d.customFields) details.customFields = d.customFields;
+ vuln.details.push(details);
+ });
+ vuln.status = 0;
+
+ Vulnerability.update(req.params.vulnerabilityId, vuln)
+ .then(msg => {
+ if (req.body.status === 2)
+ VulnerabilityUpdate.deleteAllByVuln(req.params.vulnerabilityId);
+ Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete vulnerability
+ app.delete(
+ '/api/vulnerabilities/:vulnerabilityId',
+ acl.hasPermission('vulnerabilities:delete'),
+ function (req, res) {
+ Vulnerability.delete(req.params.vulnerabilityId)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Delete all vulnerabilities
+ app.delete(
+ '/api/vulnerabilities',
+ acl.hasPermission('vulnerabilities:delete-all'),
+ function (req, res) {
+ Vulnerability.deleteAll()
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get vulnerabilities list by language
+ app.get(
+ '/api/vulnerabilities/:locale',
+ acl.hasPermission('vulnerabilities:read'),
+ function (req, res) {
+ Vulnerability.getAllByLanguage(req.params.locale)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Create or Update vulnerability from finding for validation
+ app.post(
+ '/api/vulnerabilities/finding/:locale',
+ acl.hasPermission('vulnerability-updates:create'),
+ function (req, res) {
+ if (!req.body.title) {
+ Response.BadParameters(res, 'Required parameters: title');
+ return;
+ }
+
+ var vuln = {};
+ // Required params
+ vuln.title = req.body.title;
+ vuln.locale = req.params.locale;
+
+ // Optional params
+ vuln.cvssv3 = req.body.cvssv3 || '';
+ vuln.priority = req.body.priority || null;
+ vuln.remediationComplexity = req.body.remediationComplexity || null;
+ vuln.references = req.body.references || [];
+ vuln.cwes = req.body.cwes || [];
+ vuln.vulnType = req.body.vulnType || null;
+ vuln.description = req.body.description || null;
+ vuln.observation = req.body.observation || null;
+ vuln.remediation = req.body.remediation || null;
+ vuln.category = req.body.category || null;
+ vuln.customFields = req.body.customFields || [];
+
+ VulnerabilityUpdate.create(req.decodedToken.username, vuln)
+ .then(msg => {
+ if (msg === 'Finding created as new Vulnerability')
+ Response.Created(res, msg);
+ else Response.Ok(res, msg);
+ })
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Get vulnerability updates form vuln id
+ app.get(
+ '/api/vulnerabilities/updates/:vulnId',
+ acl.hasPermission('vulnerabilities:update'),
+ function (req, res) {
+ VulnerabilityUpdate.getAllByVuln(req.params.vulnId)
+ .then(msg => Response.Ok(res, msg))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+
+ // Merge vulnerability with locale part of another one
+ app.put(
+ '/api/vulnerabilities/merge/:vulnId',
+ acl.hasPermission('vulnerabilities:update'),
+ function (req, res) {
+ if (!req.body.vulnId || !req.body.locale) {
+ Response.BadParameters(res, 'Required parameters: vulnId, locale');
+ return;
+ }
+
+ Vulnerability.Merge(req.params.vulnId, req.body.vulnId, req.body.locale)
+ .then(() => Response.Ok(res, 'Vulnerability merge successfully'))
+ .catch(err => Response.Internal(res, err));
+ },
+ );
+};
diff --git a/backend/src/translate/es_CL.json b/backend/src/translate/es_CL.json
new file mode 100644
index 0000000000000000000000000000000000000000..e5446c7a81dfd1b3cc02fcd7159aa287a408c20d
--- /dev/null
+++ b/backend/src/translate/es_CL.json
@@ -0,0 +1,24 @@
+{
+ "ok": "Arreglao",
+ "ko": "No arreglado",
+ "partial": "Arreglo parcial",
+ "unknown": "Desconocido",
+ "Attack Vector": "Vector de ataque",
+ "Attack Complexity": "Complejidad de ataque",
+ "Privileges Required": "Privilegios requeridos",
+ "User Interaction": "Interacción de usuario",
+ "Scope": "Scope",
+ "Confidentiality": "Confidencialidad",
+ "Integrity": "Integridad",
+ "Availability": "Disponibilidad",
+ "Network": "Red",
+ "Adjacent Network": "Red adyacente",
+ "Physical": "Físico",
+ "Low": "Bajo",
+ "High": "Alto",
+ "None": "Ninguno",
+ "Required": "Requerido",
+ "Unchanged": "Sin cambiar",
+ "Changed": "Cambiado",
+ "No Category": "Sin categoría"
+}
diff --git a/backend/src/translate/fr.json b/backend/src/translate/fr.json
new file mode 100644
index 0000000000000000000000000000000000000000..4e31019e32b6a53d357a7db256d0f7a05dbebca5
--- /dev/null
+++ b/backend/src/translate/fr.json
@@ -0,0 +1,20 @@
+{
+ "Attack Vector": "Vecteur d'attaque",
+ "Attack Complexity": "Complexité d'attaque",
+ "Privileges Required": "Privilèges requis",
+ "User Interaction": "Interaction utilisateur",
+ "Scope": "Portée",
+ "Confidentiality": "Confidentialité",
+ "Integrity": "Intégrité",
+ "Availability": "Disponibilité",
+ "Network": "Réseau",
+ "Adjacent Network": "Réseau Local",
+ "Physical": "Physique",
+ "Low": "Faible",
+ "High": "Haut",
+ "None": "Aucun",
+ "Required": "Requis",
+ "Unchanged": "Inchangé",
+ "Changed": "Changé",
+ "No Category": "Non Catégorisé"
+}
diff --git a/backend/src/translate/index.js b/backend/src/translate/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..5814c1b5d4c2210eb5bbb2e08632d60bbf320e3c
--- /dev/null
+++ b/backend/src/translate/index.js
@@ -0,0 +1,17 @@
+var fs = require('fs');
+var gLocale = 'en';
+
+function setLocale(locale) {
+ gLocale = locale;
+}
+exports.setLocale = setLocale;
+
+function translate(message, locale = gLocale) {
+ try {
+ let dictionary = JSON.parse(fs.readFileSync(`${__dirname}/${locale}.json`));
+ return dictionary[message] || message;
+ } catch (error) {
+ return message;
+ }
+}
+exports.translate = translate;
diff --git a/backend/src/translate/nl.json b/backend/src/translate/nl.json
new file mode 100644
index 0000000000000000000000000000000000000000..5d56a946c9561434f6fd3ccf085a513768ed474a
--- /dev/null
+++ b/backend/src/translate/nl.json
@@ -0,0 +1,22 @@
+{
+ "Attack Vector": "Aanvalsvector",
+ "Attack Complexity": "Aanvalscomplexiteit",
+ "Privileges Required": "Rechten nodig",
+ "User Interaction": "Gebruikers interactie",
+ "Scope": "Scope",
+ "Confidentiality": "Vertrouwelijkheid",
+ "Integrity": "Integriteit",
+ "Availability": "Beschikbaarheid",
+ "Network": "Netwerk",
+ "Adjacent Network": "Lokaal netwerk",
+ "Physical": "Fysiek",
+ "Low": "Laag",
+ "High": "Hoog",
+ "Medium": "Midden",
+ "Critical": "Kritiek",
+ "None": "Geen",
+ "Required": "Verplicht",
+ "Unchanged": "Ongewijzigd",
+ "Changed": "Verandert",
+ "No Category": "Geen category"
+}
diff --git a/backend/ssl/server.cert b/backend/ssl/server.cert
new file mode 100644
index 0000000000000000000000000000000000000000..4542bbc6e95bc8c625e6864de605cf1f06a55d87
--- /dev/null
+++ b/backend/ssl/server.cert
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZTCCAk2gAwIBAgIUa+pVPH6QK8QZSc+BtyGshu/OSV8wDQYJKoZIhvcNAQEL
+BQAwQjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxDjAMBgNVBAoM
+BXNoYXJ0MQ4wDAYDVQQDDAVzaGFydDAeFw0yMDAxMDgyMTQ2MzFaFw0yMDAyMDcy
+MTQ2MzFaMEIxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMQ4wDAYD
+VQQKDAVzaGFydDEOMAwGA1UEAwwFc2hhcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDGVz4FRB7YcVwXWHvLGeNdlBNy8+ap/YD8DL+un3siSvMXr65v
+zow8wJBS3O8cnSiR2FqY7CUd6rNfOluxAkyXRTC0mIMmwV92m5SF4VtdQYrG3x3o
+Z/zcYtjeYt6qT1DJB2ufodWfmkMSCXMkpEq/48mrHQh+qgv8s4S+mvI1FF8xGh73
+7We28vgVGv2IDSjO5t1iXXR78pPj7s/vU6A4NpYpacsaTVZw9KLS7jJ0IpIN4gtx
+zzZjERBp+2zVzY/mmyRVG+X3A+65fcmr8+DliNqbivBPkMQUbeGStppYF6zx7Ujg
+saibccfi8losUUblEjTckFf0NqxD5As77qBnAgMBAAGjUzBRMB0GA1UdDgQWBBSI
+KxOu1kjSFUyT1y3rVB1D0thlfjAfBgNVHSMEGDAWgBSIKxOu1kjSFUyT1y3rVB1D
+0thlfjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAC22tgmDWW
+5MKbhr3NYRgzv4I3GYKRoswnURsJaI3/viJNy6B5mVJ8Ybr+dpuUK0Q7X2H50lon
+9AZH198lyTryQTEzZBLukh25qISdY/gKiCkazYhhKXwDkqYD0E5DlEAtJOCXS3VD
+8WlFvTuSd9uKCa4gYTfAJxB7Osi+7+bXXmA9sFCRQwu3cQGaxW8drqDdAk/+6749
+dZ5XG47a52WNj0UaGfFuZsEWdTX+lpCLrVp3kYLSPHXptweL5mAG8pbzQA74ni2K
+ZM3cCdGB9kDJgD3outTN1mqnKihhWYogjVQUmC/YQlKj9AxZ2PhQTrn+7zP/urUB
+xuCmVmq01ECa
+-----END CERTIFICATE-----
diff --git a/backend/ssl/server.key b/backend/ssl/server.key
new file mode 100644
index 0000000000000000000000000000000000000000..3fb72b8fab454f75c291ea2a0f3ae6c2edbe1594
--- /dev/null
+++ b/backend/ssl/server.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDGVz4FRB7YcVwX
+WHvLGeNdlBNy8+ap/YD8DL+un3siSvMXr65vzow8wJBS3O8cnSiR2FqY7CUd6rNf
+OluxAkyXRTC0mIMmwV92m5SF4VtdQYrG3x3oZ/zcYtjeYt6qT1DJB2ufodWfmkMS
+CXMkpEq/48mrHQh+qgv8s4S+mvI1FF8xGh737We28vgVGv2IDSjO5t1iXXR78pPj
+7s/vU6A4NpYpacsaTVZw9KLS7jJ0IpIN4gtxzzZjERBp+2zVzY/mmyRVG+X3A+65
+fcmr8+DliNqbivBPkMQUbeGStppYF6zx7Ujgsaibccfi8losUUblEjTckFf0NqxD
+5As77qBnAgMBAAECggEBAId9F1f/ldajcZZdEovGjoPhYHvZU0vyaYdEqjh+p13g
+tzkKAO5NuYzSuoRwekMgtJMUqabnQd+y8X9u3S4I9Qss772epZD27eCXHRlrvb7x
+w/xgdAzL5HckayNXhOXwsq3xDLsKkWSqu4B5xhdUx99Xmu7yMVhdjzy74UVXkXdh
+0kgKczGN+0BZmPgMjtpGcCh9ERaqLltFgIXUWTm24T+160mqS+n69c7VdoyOjBSX
+OWN3K/Tze3Wr3eCtZY2x5hq+nIZJntgUCaGsvoTjVY3438Zri/4uVI+P50/Wk5eR
+U0YruysVkzyfjsascwhzyuy0chnXN1eaAkgjoOb8iZECgYEA/RuTEc1HBV9doT0x
+MUoFAp2P664AxZIQpq98rfjFhHhyig8cJaP8zJOCXd3VbpMYlSfMFuNLtZ+JMaIL
+I0yrBbgu0vUVohb58VPsh50173nKkH3GADG5t7SjhVnrBwgOc8CF3POoCgvwOxn6
+b22Pb5FpXsahHdPM0Aosw2aHSbkCgYEAyJt0vvmvAiRpYxIvU5UnP4w/OLoFhRxU
+ZUHpvDCA0E4QNWQNyZO2epZJeSXPVWPwdL/LD6HCPzV/yYdmpdhBcrFd5RnKzOW8
+LXo1R2xj9VAwvGHaxJFnY8LPhNEv4G2TsoCugkMvMqUPLVW10kzbRJrz04/y0qIY
+dL3XmBpPyx8CgYEAjC9bk36ImXcqWoWT22LNx5cRAU3Ma6DszAViHtEsRKSZe9HG
+mypqd7wzdl3JOocKFIKITRzy79M+n1jKpnBuQKq0dG793lqvcHQ1Cx+NoedoxAKF
+SdJAtHi+ILueqrRRnNbCzY++QGJAWduXk2OxD/AP1khZMDYAuDEKfecpgzECgYAi
+1LMRkaKsWYwe0oJ7HbOh2gUEIXBh60hQCIC/1yAWiudPjd7C+C2/3SZGraTxK5gN
+fVuRjEGA3hYg9KyC8Shz9I3cAH1w1Ba3QrfLfethJZpAqzDj3mc4MBP9+KA6dGwn
+myYxod6pCXW4JmCachPENq9NNXowzko2wtuTIkZmewKBgQCHrbBgAfGdLFF0zHgK
+Pz0MUYUMzO6P+l9G0QEXj5mzsqcQv9OhnCC15aFkCr7dC31bz8N6BdG6H560bupp
+XluTVNAesbdimTf0ULJ87MhdtvmqFCe1pvX20ILW89O9B6OTZJl2DAoqSYsE1GLM
+pHsUwQOzNPRI3IHdpyxKPs6sKA==
+-----END PRIVATE KEY-----
diff --git a/backend/swagger-autogen.js b/backend/swagger-autogen.js
new file mode 100644
index 0000000000000000000000000000000000000000..47d371db83d62b188ee9d797dfa3f9dfbbd9a67b
--- /dev/null
+++ b/backend/swagger-autogen.js
@@ -0,0 +1,17 @@
+const fs = require('fs');
+const swaggerAutogen = require('swagger-autogen')();
+
+const routesFolder = './src/routes/';
+
+const outputFile = '../docs/api/swagger.json';
+const endpointsFiles = [];
+
+var files = fs.readdirSync(routesFolder);
+files.forEach(file => {
+ let fileStat = fs.statSync(routesFolder + '/' + file).isDirectory();
+ if (!fileStat) {
+ endpointsFiles.push(routesFolder + '/' + file);
+ }
+});
+
+swaggerAutogen(outputFile, endpointsFiles);
diff --git a/backend/tests/audit.test.js b/backend/tests/audit.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..01358352748e1c1b63cb08b8e1736a46419ad35a
--- /dev/null
+++ b/backend/tests/audit.test.js
@@ -0,0 +1,121 @@
+/*
+ At the end
+ 1 Audit: {name: "Audit 1", language: "en", auditType: "Web"}
+*/
+
+module.exports = function (request, app) {
+ describe('Audit Suite Tests', () => {
+ var userToken = '';
+
+ var audit1Id = '';
+ var audit2Id = '';
+
+ beforeAll(async () => {
+ var response = await request(app)
+ .post('/api/users/token')
+ .send({ username: 'admin', password: 'Admin123' });
+ userToken = response.body.datas.token;
+ });
+
+ describe('Audit CRUD operations', () => {
+ it('Get Audits (no existing audit in db)', async () => {
+ var response = await request(app)
+ .get('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create audit with partial information', async () => {
+ var audit = { name: 'Audit 1' };
+ var response = await request(app)
+ .post('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(audit);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Create audit with invalid audit type', async () => {
+ var audit = {
+ name: 'Audit 1',
+ language: 'en',
+ auditType: 'Internal Test',
+ };
+ var response = await request(app)
+ .post('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(audit);
+
+ expect(response.status).toBe(404);
+ });
+
+ it('Create audit', async () => {
+ var audit = { name: 'Audit 1', language: 'en', auditType: 'Web' };
+ var response = await request(app)
+ .post('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(audit);
+
+ expect(response.status).toBe(201);
+ audit1Id = response.body.datas.audit._id;
+ });
+
+ it('Create second audit', async () => {
+ var audit = { name: 'Audit 2', language: 'fr', auditType: 'Web' };
+ var response = await request(app)
+ .post('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(audit);
+
+ expect(response.status).toBe(201);
+ audit2Id = response.body.datas.audit._id;
+ });
+
+ it('Delete audit', async () => {
+ var response = await request(app)
+ .delete(`/api/audits/${audit2Id}`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ response = await request(app)
+ .get('/api/audits')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(1);
+ });
+
+ it('Update audit general info', async () => {
+ var auditGeneralInfo = {
+ _id: audit1Id,
+ scope: ['Scope Item 1', 'Scope Item 2'],
+ };
+
+ var response = await request(app)
+ .put(`/api/audits/${audit1Id}/general`)
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditGeneralInfo);
+
+ expect(response.status).toBe(200);
+ });
+
+ it('Get audit general info', async () => {
+ var response = await request(app)
+ .get(`/api/audits/${audit1Id}/general`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+
+ expect(response.body.datas.name).toBe('Audit 1');
+ expect(response.body.datas.auditType).toBe('Web');
+ expect(response.body.datas.language).toBe('en');
+ expect(response.body.datas.collaborators).toHaveLength(0);
+ expect(response.body.datas.reviewers).toHaveLength(0);
+ expect(response.body.datas.customFields).toHaveLength(0);
+ expect(response.body.datas.scope).toHaveLength(2);
+ expect(response.body.datas.scope[0]).toBe('Scope Item 1');
+ expect(response.body.datas.scope[1]).toBe('Scope Item 2');
+ });
+ });
+ });
+};
diff --git a/backend/tests/client.test.js b/backend/tests/client.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..c39bffcf419cbcf6d1f161c2f451cf2510c1bc52
--- /dev/null
+++ b/backend/tests/client.test.js
@@ -0,0 +1,198 @@
+/*
+ At the end
+ 2 Clients: [
+ {
+ email: "client_updated@example.com",
+ lastname: "Client",
+ firstname: "Updated",
+ phone: "5146669999",
+ cell: "4389996666",
+ title: "IT manager",
+ company: {name: 'New Updated Company'}
+ },
+ {
+ email: "client2@example.com",
+ lastname: "Client",
+ firstname: "User",
+ phone: "5146669999",
+ cell: "4389996666",
+ title: "IT manager",
+ company: {name: 'Company 1'}
+ }
+ ]
+
+ 2 more Companies created:
+ 'New Company'
+ 'New Updated Company'
+*/
+
+module.exports = function (request, app) {
+ describe('Client Suite Tests', () => {
+ var userToken = '';
+ beforeAll(async () => {
+ var response = await request(app)
+ .post('/api/users/token')
+ .send({ username: 'admin', password: 'Admin123' });
+ userToken = response.body.datas.token;
+ });
+
+ describe('Client CRUD operations', () => {
+ var client1Id = '';
+ var client2Id = '';
+ var client3Id = '';
+ it('Get clients (no existing clients in db)', async () => {
+ var response = await request(app)
+ .get('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create client with email only', async () => {
+ var client = { email: 'client1@example.com' };
+ var response = await request(app)
+ .post('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+ expect(response.status).toBe(201);
+ client1Id = response.body.datas._id;
+ });
+
+ it('Create client with all information and existing company', async () => {
+ var client = {
+ email: 'client2@example.com',
+ lastname: 'Client',
+ firstname: 'User',
+ phone: '5146669999',
+ cell: '4389996666',
+ title: 'IT manager',
+ company: { name: 'Company 1' },
+ };
+ var response = await request(app)
+ .post('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+ expect(response.status).toBe(201);
+ client2Id = response.body.datas._id;
+ });
+
+ it('Create client with nonexistent company', async () => {
+ var client = {
+ email: 'client3@example.com',
+ company: { name: 'New Company' },
+ };
+ var response = await request(app)
+ .post('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+ expect(response.status).toBe(201);
+ client3Id = response.body.datas._id;
+ });
+
+ it('Should not create client with existing email', async () => {
+ var client = { email: 'client1@example.com' };
+ var response = await request(app)
+ .post('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Should not create client without email', async () => {
+ var client = { firstname: 'firstname', lastname: 'lastname' };
+ var response = await request(app)
+ .post('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get clients (existing clients in db)', async () => {
+ const expected = [
+ { email: 'client1@example.com' },
+ {
+ email: 'client2@example.com',
+ lastname: 'Client',
+ firstname: 'User',
+ phone: '5146669999',
+ cell: '4389996666',
+ title: 'IT manager',
+ company: { name: 'Company 1' },
+ },
+ {
+ email: 'client3@example.com',
+ company: { name: 'New Company' },
+ },
+ ];
+ var response = await request(app)
+ .get('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(
+ response.body.datas.map(t => {
+ return {
+ email: t.email,
+ lastname: t.lastname,
+ firstname: t.firstname,
+ phone: t.phone,
+ cell: t.cell,
+ title: t.title,
+ company: t.company,
+ };
+ }),
+ ).toEqual(expect.arrayContaining(expected));
+ });
+
+ it('Update client', async () => {
+ var client = {
+ email: 'client_updated@example.com',
+ lastname: 'Client',
+ firstname: 'Updated',
+ phone: '5146669999',
+ cell: '4389996666',
+ title: 'IT manager',
+ company: { name: 'New Updated Company' },
+ };
+
+ var response = await request(app)
+ .put(`/api/clients/${client1Id}`)
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+ expect(response.status).toBe(200);
+ });
+
+ it('Update client with nonexistent id', async () => {
+ var client = { firstname: 'Client' };
+
+ var response = await request(app)
+ .put(`/api/clients/deadbeefdeadbeefdeadbeef`)
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(client);
+ expect(response.status).toBe(404);
+ });
+
+ it('Delete client', async () => {
+ var response = await request(app)
+ .delete(`/api/clients/${client3Id}`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ response = await request(app)
+ .get('/api/clients')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(2);
+ });
+
+ it('Delete client with nonexistent email', async () => {
+ var response = await request(app)
+ .delete(`/api/clients/deadbeefdeadbeefdeadbeef`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(404);
+ });
+ });
+ });
+};
diff --git a/backend/tests/company.test.js b/backend/tests/company.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..c64e901345299ee319291126925b9128b802c1b5
--- /dev/null
+++ b/backend/tests/company.test.js
@@ -0,0 +1,118 @@
+/*
+ At the end
+ 1 Company: {name: 'Company 1', logo: 'fsociety logo'}
+*/
+
+module.exports = function (request, app) {
+ describe('Company Suite Tests', () => {
+ var userToken = '';
+ beforeAll(async () => {
+ var response = await request(app)
+ .post('/api/users/token')
+ .send({ username: 'admin', password: 'Admin123' });
+ userToken = response.body.datas.token;
+ });
+
+ describe('Company CRUD operations', () => {
+ var logo =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAUp0lEQVR4nO2dBZAjtxKG+yCcC9MFKsx4YWZm5qTCzMy5MDMzMzNzLgwXZmZmplefyr2lnTc8tiR7/Fe5dtc7nhlLrVb33zB9Bg4c+J90UVv09X0DXfhFVwBqjq4A1BxdAag5ugJQc3QFoOboCkDN0RWAmqMrADVHVwBqjv6+b8A1+vTp0/MC//3Xmwnn7+h7nYyOFIC+ffvKJJNMItNMM41MOeWU5vcJJphAxhlnHBlttNFkpJFGkmGHHdYc++eff5qf//77r/z+++/y888/y7fffitffvmlfPLJJ/LBBx/I22+/LW+88YZ8/vnnnr9Z89GnE4JB/fv3l5lnnlnmn39+mWuuuWTQoEEyyiij9KzyZgCt8MUXX8jzzz8vTz31lAwZMkReffVVIzjtjLYVAFbwAgssIMsvv7wstthiMuaYY/b8r5kTHwfdIhCIe+65R2699VZ54okn5K+//mrpdVuBthOAqaaaStZZZx1ZZZVVZOyxxzbvtXrCs4BAfPrpp3LttdfKFVdcIR9++KHX+ymCthAA9vT55ptPttpqK7PqUfmhApvijjvukNNPP11eeukl37eTiaAFgJXNvr7bbrvJbLPN1st6DxW6Pfz9999GEI444ghjSIaKYAVg6qmnlv32208WWWQRowHaFXgVJ5xwgpxzzjlGKEJDcAIw3HDDyY477mjUPb9LAHt8FahGwGvge3322We+b6kXghKAiSee2Oyds8wyS1tPehwQBCZ/iy22kOeee8737fQgGN0655xzys0332x8+E6bfGlosfHHH1+uvPJK47aGgiAEYKGFFpJLL71UxhprLN+30nKMPPLIcvbZZ8uSSy7p+1YMvAsAzB0DwsB04sqPwwgjjGC2unnnndf3rfi1AeDoUft1WPlxIOaw8sorm1iDL3jTAMMPP7yceeaZvSjcumGMMcaQs846y2g/X/AmALvvvrvMNNNMtVH7SSBiecghh3gbBy8CwMRvuummPi4dJFZffXVZaqmlvFzbuQAg6TB8wwwzjOtLBwnGA6bzoIMO8rIVOBeAWWed1Vi/dVf9NhiLiSaayIS2XcO5AMw444xtze23EriHruFlC+giHHSXYs3RFYCAoAmqLtFvwIABg11e8JtvvpF7771XPv74Y8MEquVb163hn3/+MaHi0047Ta677jrnOQNeqeABAwbIwQcfLGuuuWYtBYDwMNlODzzwgLd78J4P0K9fPxMJJCKYBGLpv/76qzz22GPy9NNPy+uvvy5ff/21yQ2ETp100klNDgHpY/zdCmHSgpGffvpJHnzwQXnmmWfkzTfflB9//NFcjwTVySabzISzF1xwQRl11FHN55LuhZqD1VZbzXsCqffsSlTgiSee2EsA7Moc8umIGdx4441msNNAqjhh1h122EFmmGGGyvdm3wdZv0Twrr76avnll19SP4c7t+yyy8q2225rUtskRhCOPfZY75MvIWgAaUwcK8qOCn7//fdmkC655JLC+fZolU022UT23ntvE3QqCwQAASWf77jjjjNaqOh9rL322rLPPvvI6KOP3vM+xSQkwCBU0eO5nks4FwCyYlCjvGxcddVVJuWbwUHF7rrrrqbwwgZqdvHFFzeCcuedd8pbb72Vei1YR3INBg4cWHhbYPK/++47k8d33333pR473njjyXLLLSd//PGH3H///f83seOOO64cc8wxJhOI8yLQxEN0DNjKSH4dccQR5aabbip0n1Xh3A1ERbPasXrZKxUMGoODqt944417TT776hlnnCFPPvmk0Qp77bWX3H333bLTTjuZVZMEcu9WWGEFefzxxwsXfb744ovms1mTT5HKQw89ZIzZo48+2lzrwgsvNN9NhY7vstFGG5mtjPfQeNQngmmnnVauueYaOf/883tpCVdwrgGo4eOLvvPOO6YAk1UDGLQ55phD5p577p6VgbYYPHiwLL300rHFIEzoLbfcIjvvvLP89ttvidfks+uuu66xDdAGkmCccT6My4svvtgIKMWiaedkiyHJE2o7ej402SOPPGLU/3vvvWfeIwCGhsBoJR6iNgCfJxZAPaPrYpIgbABw8sknm1VBmJhVTYj0wAMPNNW8aWDShg4dKltuuaXhFtKAPYA2IRppZx5rxu6hhx4qd911V6owASz8k046SZZYYonErUW1DXUBaAaECtXPd0Kg0RAh1BIGIwDU+h1wwAFmC8Cdm2666XLv2ww22gQhoHI3C6xA1LZqFT6/xhprGDczC5BXqOsk6z7u3sC7775rti3sAFQ+GiYEOBcAij3U0kVNank1/juThyFUFqxcVC4GZRrQMPAJGG96H1NMMUWqygfzzDOP2ccxQqtwDQjBa6+9ZvoVIFCwf2g/rs99uYRzIxDW79lnnzWTjdqn2hf88MMPJkmyCvC/cdfQJGkJJ2wr0XJyJiIJ/H/99deXyy67rKciuQpU82D/UPR63nnnmXqBZnAXReE8FsCEEw/AasbH53cGmD2fV1UWj89TSMrr0UcfNXtwFNgZEE/2tTDEiFFEgUbCNthll116hKrqPWLc3n777YZQoq/A5Zdfbu4Touvll1+udO6icL4FMHi2O4ZVjmFENoxtlKEKsZ7XWmut0gOO+6Uuo14TtwtfO5p+hUGGS2fbARiK+O/TTz99qeuj2jH+sDkw+myXlevAddiVw2yP6hW5gnMNoGDFwYWj/nAN1ZViP8a9oqyaQElW2riydRKzMtlXV1xxRaNaIY1Q/RdddFHsHs7kLLroooajQEXvv//+JlsXEid6LNfjPrPK1flOygbSX4i9X4+fcMIJe4QbzgFhcc0Cii8vgCJQkiBh9aKDSIcNVgY8+r777vt/n9UJx4q/4YYb5IUXXjCqk/2fYAyTiEVv9whSEohJy2ouwTEca69W/oaaRlVzXQo5uAeugXZYaaWVjEuYxFXAJtI9hBoAW9Pp9WhAhQfjo0DEuQAsvPDChltXJswGgwolCknEfoy6VqgKZ8JR66yaJGCoUZPPuaTins11oaZhHb/66qvE4xAE/H22DfuafJ4eQkwwXgSCYN+Pfi+EeOuttzbXcgnnXgAGWNzkg48++sisAlafegc2oGVXXXXV1MkHTBTBIFi3Kj3/+CzngJpOm3zwyiuvmC2NGIUNJltJJ7yfKMmkGhByCbrYNZwKABz47LPPnvh/DbNiJUczh2Hqtt9++0xfXUF6Fas2GlBiUhEk3lfh4CdqGK/BPo5jOEfeVC3uDXUfbQmDHSENozAt4wcq3HW9hFMBoGkjWUBJYA9nJaiBZYOkEbiCIsDFPOqoo3ppAc4NFYyxZ18D1QuHoOAzxx9/vDlHEeDORYkonXTKwNKKP/juEFIu4VQAsmoCMOQ22GADE4aNqm62hzKAWoYm1vOxmlHn9n0gCEwMgSA9jt/Zr8sg2gZGCS7a3mR5Da7JIKcCYBt1SSBiByvHalfrna2h6EqUxoASKbTdPiaDxI6oqsVFxFXTPRr3c5tttkkNNycBAcCoU2HiuxBaxn5JEwCOzzNGzYTzLSALMG+EeHWrgBAi2UKtYyaECWWlYE+gVuOMSt4j4ILdYE+iRgyjlC7HIGhoH2kID+zfKaecEptVxHtsWTS4ILsH11aFijDwMsssY9rKSkPwuZc8FVF5xqiZcJoTyCBlgUHSAYd7hwvAv8Z/JoxKvgCrE4NSVxMrjEQMeAFat2KMcXzU55ZGMqaq/Oh1pRG1wwjVz7Fy8QTYDphguAsoayaevAY9DtuCrQaG77bbbjMCy3FHHnmk4SXyTD7ngjV0CWcCwArTZIy8wKXabrvtZMMNN0xtC4vxhHDwYoUTq8cQw/reY489evnkqgGipI3mHdjuHpOKEYlgoYXoacAKjbsHJpjoImFtVD3biSazIgB5wRi5zA10RgQxGVjeRfZUBiEu2yYNuu/C2DH50MlEIHmfSYHvJ+OIlap9CAHvocqJ82t6FnF7YgGHH364oXGLFLXaLmaRDqd8Z3IZs3iHZsGZDUC8P+8AqvFXhsHTwYZxJAgENcxKJgCEhsCbIK/QnnxpFKkQlCKPEFpXkzf4yd9FK5rLtrXlOi7b5jgTAFRskQwfVCf9+KuArYHrEs4ltsDfrGqEIw5oigsuuMDc52abbWa4B/j+KsAuwK7IC66dlQbXTDgTgDQCyAZu2uabb26KRZrhEh122GFmLyYiRwwe9ZokiKw+NAaGJIUdcPsEcKqArB8SVNA6hHrzUNNVha4InAlAVqoXA0McAMudLttMWNH9PwrSyAk8kRHMZCbFIGzoCsRtY8vgcwSnyoLzQW7BKmIMavp7Glw2inAmAPqMniSQI4f1/P7775tBw+qumnlDmhUu5Z577mmMz7zn4ziOJy8Bo+z6668vHVTiXPpEE4xg8hMIHKWdL2qftBLOBCDJiFLXjFUC/SqNShvy5KsCRg6ypkwen/rkfDYaUCoK+AOqnvSe0EgUliYJgcsWOs6ulOTXws1Dudr8ebPYMFYdpFDZBzvpU8Sa0ckUxlKBoBMWJ8kkDi4fROVMAOIKK1kBGGioRhvNcoPIwyOGAMVbRoVrPII6haqIGsF4BiTGagaSjazq42bCGRMYl/LNqo8rkKjCgtlRP0KrROCgcSF5sopN9LMkbsBCMhHE99kG9H9l7ZK4YBYdQUg2IUpqQ+MRLuBMAGDhkHZ7f6MeMC5tW2vpioLzY2AxsASUuKZOHNQvcQVSs5JA3J5EULgAWwjh8wk+QUmT/5fHm7DBPRCriLseSarRPISqNkcROKOCsWzp7MFPviQrlKKIaCm1NOIGZO3EpYXZ0Mlle4Gvp84AAUjaQ7k23cmZzGheHi/8dWjgNEBpY7Cut956xsfP0gjcC+wiXcHj7ovzUSSjkUTGBXshb+ZTVTizASBB7FQpCiDiJl8aWwAJkqRyp+3dsGyEa9nr4f2prE0zoLgH8gNsdcz5WYlUKbHys8DqhNTBLuCaCHXSPfI+3wEWMum+4Pz5Hgoqhl1NvrgOBzNBuqqzyqDhBeglAG1LNi159KwSDDO2CAxHcgWKDhb0MiwftgGagAkkFZ1s4yLAPiBcDdeAMMBeEiJGI5AMwvmwI9A4adXGCAZ7Ps82RmBcVwY5FQBUoWbF5MnwYcVSrs2rmYB3IC+wGdAaBV6wnXw3tqQiXocdmnb9QCmnAkAdnA5MFjPYjijaQ0gaJJG6vYwNY+QSTlPC7Eew58kOqgNQ/SoA2ANsfS7hVADY74i0oSaJyoX8DGBXoHpJC2ZJPXNdH+i8MohmT2TfYNQhBHUGnAh5g9JQ/647hIkPAcDNofgTfxc+vI4tYhXwIFRGA9hKsoldw0t5OJEw1D9kysMPPxzc83RdAOPv1FNPNUmgpI/TbczHw6W9tYuHwKHIE5q1KLXaCUD7sfrhAMh8dkn+2PAmAMrKEfdHGFymQfkGhBEp5hjFJKuQBOML3jqESCNCCGtGgIZUMMghwqQu4+EuQXYSJeQEfwgPE79A+H3Ce59A9kKSNbUPDzQvkTxcInziaE/htPNQ7UM2EZk8hFjxMuj9g51BoCirAaSC7GHqCUjlQkBxXaF0iWVwvrwsH7l9BHtQ9dDFnE9bzhAFxQWkUsknvAsA4KGJROHsCh5pMGsYiASN2Cv5G0OJaCGRPWwH/Gb4fGmEVzkWwomYAYOOm0WDBo6h6wheCAEdzodAcE0mii2IzB8EiLo+hIcJpy0MnD40L8mi/OTarGaidhA5XJd7xrDlfQQIcoc8An63v5f+fu6555roo28EIQD4w8TwSdooWgXERBKjT8rc5XysOvZdGjAQACJPEG3BhPF/hAibBOEhWERCCJVDRPLSiBk4Da5dFFyL1e9z71cEIQCAgeSBDGXaujNpZNvmVfEIHHswGoTfscBR9UUe2oQwUWRSJqZBcItMoBAQzFPDKMEqkwmDwJDqRVOovHWHGJlsEax4IoMkaRaZfKqHSWUr286FMHIoCEYAWL0YW2WAEFDGjRC0usfO5JNPblY+VHYZFhPPx+5F5BvBCADQBzuUBS4W1n7eMrQiYLLZZmj5ltZXOAt8R1+kTxyCEoCyfYDEqsbF8sdqJ4uoWcBoxGrHRrGt+jLI087eJYISgGbEBDDq4AFoEEGjiLIdN5hk/HeKVOEkcA2rVuyg3YqmnrUawXgB0iBgsOibESHUrQTihgczQuRQLIqQJW0zcAEYlGgRJpxmEWXr/JPuCXc068kmLhGUAECiQNi0Kl0MYYBuZqvhJ/44XAAED5Y9JJD2J2pFmJrrIWB53VUXCColhwkiRRoLuxXAQ2CS9UkhroH2cd0OPgtB2QCwbnEVNJ0C8iBCC3QFJQCAQFCnwnXKdx4EJwDk14dkJDULGIBEJUNDcAKAHcBTRKoQQqGB70KYO6vNvQ8EJwAANk/rBzoFdBwLbf+XUAWAuD/PDOoU4Nkg1CEiSAGQRvOEqk/8CAGselrVuez6UQTBCgATT1oW5VLtKATac4CWdwhzqAhWAKTRVYQGUqGRJ3mB348QhyzAXrOC8wDaFkGIe8RciNCVT7Injant5g8hIngBkMYTueDP6QQSsgDoSifxlF6A0YdHhYi2EABpdO4iG5dcPGlRsKYqEABS2Zn8KrkNLtE2AiCNbBr6+A8aNMj8HYoQ6MqnuQOdxFz1+m8G2koAGGioYqJ6PC/IZUvVLBDDoO9Q3kKWUNBWAiANIRgyZIiprCFxw6dhqM8jhrqmsXQ7eittJwAKbAIKN+giZj9AyhW0vRwkD7V+Pp783QwElRFUBmTYkKyZ9DCnVgENxGPlyBJuZ4SziZaE9v2j7azW6LWKeNFzDx061LS7a/fJl07QADZ4xAydNigSyXpCSVEw8TS3RNvwXMB2VflRdJQAKEjyRCvw4kGT+giWooWnvCgfo2yNWn4yekKmdcugIwXABlm+5PdTIo6GoKqHpFD4BLQE9YQkoRCto2wLCpdnF9GylXa0/N5pk26j4wUgDrbraNfut9J+CBVBpYW7Qh0nOglt7wV0UQ1dAag5ugJQc3QFoOboCkDN0RWAmqMrADVHVwBqjq4A1BxdAag5/gfOJVH+CNY0UQAAAABJRU5ErkJggg==';
+ var company1Id = '';
+ var company2Id = '';
+ it('Get companies (no existing companies in db)', async () => {
+ var response = await request(app)
+ .get('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create company with name only', async () => {
+ var company = { name: 'Company 1' };
+ var response = await request(app)
+ .post('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(company);
+
+ expect(response.status).toBe(201);
+ company1Id = response.body.datas._id;
+ });
+
+ it('Create company with name and logo', async () => {
+ var company = { name: 'Company 2', logo: 'VGVzdCBpbWFnZQ==' };
+ var response = await request(app)
+ .post('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(company);
+
+ expect(response.status).toBe(201);
+ company2Id = response.body.datas._id;
+ });
+
+ it('Should not create company with existing name', async () => {
+ var company = { name: 'Company 1' };
+ var response = await request(app)
+ .post('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(company);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get companies (existing companies in db)', async () => {
+ const expected = [
+ { name: 'Company 1' },
+ { name: 'Company 2', logo: 'VGVzdCBpbWFnZQ==' },
+ ];
+ var response = await request(app)
+ .get('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(
+ response.body.datas.map(t => {
+ return { name: t.name, logo: t.logo };
+ }),
+ ).toEqual(expect.arrayContaining(expected));
+ });
+
+ it('Update company with logo only', async () => {
+ var company = { logo: logo };
+ var response = await request(app)
+ .put(`/api/companies/${company1Id}`)
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(company);
+ expect(response.status).toBe(200);
+ });
+
+ it('Update company with nonexistent id', async () => {
+ var company = { name: 'company Updated' };
+
+ var response = await request(app)
+ .put(`/api/companies/deadbeefdeadbeefdeadbeef`)
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(company);
+ expect(response.status).toBe(404);
+ });
+
+ it('Delete company', async () => {
+ var response = await request(app)
+ .delete(`/api/companies/${company2Id}`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ response = await request(app)
+ .get('/api/companies')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(1);
+ });
+
+ it('Delete company with nonexistent id', async () => {
+ var response = await request(app)
+ .delete(`/api/companies/deadbeefdeadbeefdeadbeef`)
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(404);
+ });
+ });
+ });
+};
diff --git a/backend/tests/data.test.js b/backend/tests/data.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..ac2e73a753f224bf6086fb14a61203f8f1fe5a1f
--- /dev/null
+++ b/backend/tests/data.test.js
@@ -0,0 +1,493 @@
+/*
+ At the end
+ 2 Languages: [
+ {locale: 'en', language: 'English'},
+ {locale: 'fr', language: 'French'}
+ ]
+ 1 Audit type: {locale: 'en', name: 'Web'}
+ 1 Vulnerability type: {locale: 'en', name: 'Internal'}
+ 3 Sections: [
+ {locale: 'en', name: 'Attack Scenario', field: 'attack_scenario'},
+ {locale: 'en', name: 'Goal', field: 'goal'},
+ {locale: 'fr', name: 'But', field: 'goal'}
+ ]
+*/
+
+module.exports = function (request, app) {
+ describe('Data Suite Tests', () => {
+ var userToken = '';
+ beforeAll(async () => {
+ var response = await request(app)
+ .post('/api/users/token')
+ .send({ username: 'admin', password: 'Admin123' });
+ userToken = response.body.datas.token;
+ });
+
+ describe('Language CRUD operations', () => {
+ it('Get languages', async () => {
+ var response = await request(app)
+ .get('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create 3 languages', async () => {
+ var english = {
+ locale: 'en',
+ language: 'English',
+ };
+
+ var french = {
+ locale: 'fr',
+ language: 'French',
+ };
+
+ var espagnol = {
+ locale: 'es',
+ language: 'Espagnol',
+ };
+ var response = await request(app)
+ .post('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(english);
+ expect(response.status).toBe(201);
+
+ var response = await request(app)
+ .post('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(french);
+ expect(response.status).toBe(201);
+
+ var response = await request(app)
+ .post('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(espagnol);
+ expect(response.status).toBe(201);
+ });
+
+ it('Should not create with existing locale', async () => {
+ var language = {
+ locale: 'fr',
+ language: 'French2',
+ };
+ var response = await request(app)
+ .post('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(language);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Should not create with existing name', async () => {
+ var language = {
+ locale: 'us',
+ language: 'English',
+ };
+ var response = await request(app)
+ .post('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(language);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get languages', async () => {
+ const expected = [
+ { locale: 'en', language: 'English' },
+ { locale: 'fr', language: 'French' },
+ { locale: 'es', language: 'Espagnol' },
+ ];
+
+ var response = await request(app)
+ .get('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toEqual(expect.arrayContaining(expected));
+ });
+
+ it('Delete language', async () => {
+ var response = await request(app)
+ .delete('/api/data/languages/es')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ var response = await request(app)
+ .get('/api/data/languages')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(2);
+ });
+
+ it('Should not delete language with nonexistent locale', async () => {
+ var response = await request(app)
+ .delete('/api/data/languages/us')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(404);
+ });
+ });
+
+ describe('Audit types CRUD operations', () => {
+ it('Get audit types', async () => {
+ var response = await request(app)
+ .get('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create audit type Retest', async () => {
+ // Get the template ID first
+ response = await request(app)
+ .get('/api/templates')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ var templates = response.body.datas;
+
+ var auditType = {
+ name: 'Retest',
+ templates: templates,
+ stage: 'retest',
+ };
+
+ var response = await request(app)
+ .post('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditType);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Create audit type Multi', async () => {
+ // Get the template ID first
+ response = await request(app)
+ .get('/api/templates')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ var templates = response.body.datas;
+
+ var auditType = {
+ name: 'Multi',
+ templates: templates,
+ stage: 'multi',
+ };
+
+ var response = await request(app)
+ .post('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditType);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Create audit type with wrong stage', async () => {
+ // Get the template ID first
+ response = await request(app)
+ .get('/api/templates')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ var templates = response.body.datas;
+
+ var auditType = {
+ name: 'Wifi',
+ templates: templates,
+ stage: 'itdoesnotexist',
+ };
+
+ var response = await request(app)
+ .post('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditType);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Create audit type Web', async () => {
+ // Get the template ID first
+ response = await request(app)
+ .get('/api/templates')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ var templates = response.body.datas;
+
+ var auditType = {
+ name: 'Web',
+ templates: templates,
+ };
+ var response = await request(app)
+ .post('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditType);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Should not create with existing name', async () => {
+ // Get the template ID first
+ response = await request(app)
+ .get('/api/templates')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ var templates = response.body.datas;
+
+ var auditType = {
+ name: 'Web',
+ templates: templates,
+ };
+ var response = await request(app)
+ .post('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(auditType);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get audit types', async () => {
+ const expected = [
+ {
+ hidden: ['network'],
+ name: 'Retest',
+ sections: [],
+ templates: [{}],
+ stage: 'retest',
+ },
+ {
+ hidden: ['network'],
+ name: 'Multi',
+ sections: [],
+ templates: [{}],
+ stage: 'multi',
+ },
+ {
+ hidden: [],
+ name: 'Wifi',
+ sections: [],
+ templates: [{}],
+ stage: 'default',
+ },
+ {
+ hidden: [],
+ name: 'Web',
+ sections: [],
+ templates: [{}],
+ stage: 'default',
+ },
+ ];
+ var response = await request(app)
+ .get('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toEqual(expect.arrayContaining(expected));
+ });
+
+ it('Delete audit type', async () => {
+ var response = await request(app)
+ .delete('/api/data/audit-types/Wifi')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ var response = await request(app)
+ .get('/api/data/audit-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(3);
+ });
+
+ it('Should not delete audit type with nonexistent name', async () => {
+ var response = await request(app)
+ .delete('/api/data/audit-types/nonexistent')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(404);
+ });
+ });
+
+ describe('Vulnerability types CRUD operations', () => {
+ it('Get vulnerability types', async () => {
+ var response = await request(app)
+ .get('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create vulnerability type Internal', async () => {
+ var type = {
+ locale: 'en',
+ name: 'Internal',
+ };
+ var response = await request(app)
+ .post('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(type);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Create vulnerability type Web', async () => {
+ var type = {
+ locale: 'en',
+ name: 'Web',
+ };
+ var response = await request(app)
+ .post('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(type);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Should not create with existing name', async () => {
+ var type = {
+ locale: 'en',
+ name: 'Web',
+ };
+ var response = await request(app)
+ .post('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(type);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get vulnerability types', async () => {
+ const expected = [
+ { locale: 'en', name: 'Internal' },
+ { locale: 'en', name: 'Web' },
+ ];
+ var response = await request(app)
+ .get('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toEqual(expect.arrayContaining(expected));
+ });
+
+ it('Delete vulnerability type', async () => {
+ var response = await request(app)
+ .delete('/api/data/vulnerability-types/Web')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(200);
+
+ var response = await request(app)
+ .get('/api/data/vulnerability-types')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.body.datas).toHaveLength(1);
+ });
+
+ it('Should not delete vulnerability type with nonexistent name', async () => {
+ var response = await request(app)
+ .delete('/api/data/vulnerability-types/nonexistent')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+ expect(response.status).toBe(404);
+ });
+ });
+
+ describe('Sections CRUD operations', () => {
+ it('Get sections', async () => {
+ var response = await request(app)
+ .get('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toHaveLength(0);
+ });
+
+ it('Create section Attack Scenario locale en', async () => {
+ var section = {
+ name: 'Attack Scenario',
+ field: 'attack_scenario',
+ };
+ var response = await request(app)
+ .post('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(section);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Create section But locale fr', async () => {
+ var section = {
+ name: 'But',
+ field: 'goal',
+ };
+ var response = await request(app)
+ .post('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(section);
+
+ expect(response.status).toBe(201);
+ });
+
+ it('Should not create section with existing name', async () => {
+ var section = {
+ name: 'Attack Scenario',
+ field: 'goal',
+ };
+ var response = await request(app)
+ .post('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(section);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Should not create section with existing field', async () => {
+ var section = {
+ name: 'But2',
+ field: 'goal',
+ };
+ var response = await request(app)
+ .post('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`])
+ .send(section);
+
+ expect(response.status).toBe(422);
+ });
+
+ it('Get sections', async () => {
+ const expected = [
+ { name: 'Attack Scenario', field: 'attack_scenario' },
+ { name: 'But', field: 'goal' },
+ ];
+ var response = await request(app)
+ .get('/api/data/sections')
+ .set('Cookie', [`token=JWT ${userToken}`]);
+
+ expect(response.status).toBe(200);
+ expect(response.body.datas).toEqual(expect.arrayContaining(expected));
+ });
+
+ //it('Should not delete nonexistent section', async () => {
+ // var response = await request(app).delete('/api/data/sections/attack_scenario/ru')
+ // .set('Cookie', [
+ // `token=JWT ${userToken}`
+ // ])
+ // expect(response.status).toBe(404)
+ //})
+
+ //it('Delete section', async () => {
+ // const expected = [
+ // {locale: "en", name: 'Attack Scenario', field: 'attack_scenario'},
+ // {locale: "fr", name: 'Scenario', field: 'attack_scenario'},
+ // {locale: "en", name: 'Goal', field: 'goal'},
+ // ]
+
+ // var response = await request(app).delete('/api/data/sections/but/fr')
+ // .set('Cookie', [
+ // `token=JWT ${userToken}`
+ // ])
+ // expect(response.status).toBe(200)
+
+ // var response = await request(app).get('/api/data/sections')
+ // .set('Cookie', [
+ // `token=JWT ${userToken}`
+ // ])
+ // expect(response.body.datas).toHaveLength(3)
+ // expect(response.body.datas).toEqual(expect.arrayContaining(expected))
+ //})
+ });
+ });
+};
diff --git a/backend/tests/index.test.js b/backend/tests/index.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..0be4a12feb0193f4421162af488c3f0befdf6a19
--- /dev/null
+++ b/backend/tests/index.test.js
@@ -0,0 +1,27 @@
+const request = require('supertest');
+
+var env = process.env.NODE_ENV || 'dev';
+var config = require('../src/config/config.json')[env];
+
+var mongoose = require('mongoose');
+mongoose.connect(
+ `mongodb://${config.database.server}:${config.database.port}/${config.database.name}`,
+ {},
+);
+
+/* Clean the DB */
+mongoose.connection.dropDatabase();
+
+const app = require(__dirname + '/../src/app');
+
+// Import tests
+require('./unauthenticated.test')(request, app);
+require('./user.test')(request, app);
+require('./template.test')(request, app);
+require('./data.test')(request, app);
+require('./company.test')(request, app);
+require('./client.test')(request, app);
+require('./vulnerability.test')(request, app);
+require('./audit.test')(request, app);
+require('./settings.test')(request, app);
+require('./lib.test')();
diff --git a/backend/tests/lib.test.js b/backend/tests/lib.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..cee6fc659e31e80e129801be5f0592aa80f84623
--- /dev/null
+++ b/backend/tests/lib.test.js
@@ -0,0 +1,571 @@
+module.exports = function () {
+ var html2ooxml = require('../src/lib/html2ooxml');
+ var utils = require('../src/lib/utils');
+
+ describe('Lib functions Suite Tests', () => {
+ describe('Name format validation tests', () => {
+ it('Valid Filename', () => {
+ var filename = 'Vulnerability 1';
+ var result = utils.validFilename(filename);
+ expect(result).toEqual(true);
+ });
+
+ it('Valid Latin Filename', () => {
+ var filename = 'Vulnerabilité 1';
+ var result = utils.validFilename(filename);
+ expect(result).toEqual(true);
+ });
+
+ it('Valid Latvian Filename', () => {
+ var filename = 'Pažeidžiamumas 1';
+ var result = utils.validFilename(filename);
+ expect(result).toEqual(true);
+ });
+
+ it('Valid Filename with special chars', () => {
+ var filename = 'Vulnerability_1-test';
+ var result = utils.validFilename(filename);
+ expect(result).toEqual(true);
+ });
+
+ it('Invalid Filename', () => {
+ var filename = '
StrikeMarkHeading
';
+ var expected =
+ `Heading
';
+ var expected =
+ `Heading
';
+ var expected =
+ `Heading
';
+ var expected =
+ `Heading
';
+ var expected =
+ `Heading
';
+ var expected =
+ `` +
+ `
`;
+ var expected =
+ `` +
+ `
`;
+ var expected =
+ `` +
+ `
` +
+ `` +
+ `
`;
+ var expected =
+ `` +
+ `
`;
+ var expected =
+ `` +
+ `
` +
+ `
BreakCode
Paragraph
';
+ var expected =
+ `Code Block
+
AuditForge
+
+
+
+
+
+
+
+
+
0&&(d=c.substr(-1*Math.floor(s/2))),(c.substr(0,Math.ceil(s/2))+n+d).substr(0,s+r)}(e,n):"middle"===r?function(e,t,n){if(e.length<=t)return e;var r,o;null==n?(n="…",r=8,o=3):(r=n.length,o=n.length);var a=t-o,i="";return a>0&&(i=e.substr(-1*Math.floor(a/2))),(e.substr(0,Math.ceil(a/2))+n+i).substr(0,a+r)}(e,n):function(e,t,n){return function(e,t,n){var r;return e.length>t&&(null==n?(n="…",r=3):r=n.length,e=e.substring(0,t-r)+n),e}(e,t,n)}(e,n)},e}(),c=function(){function e(e){this.__jsduckDummyDocProp=null,this.matchedText="",this.offset=0,this.tagBuilder=e.tagBuilder,this.matchedText=e.matchedText,this.offset=e.offset}return e.prototype.getMatchedText=function(){return this.matchedText},e.prototype.setOffset=function(e){this.offset=e},e.prototype.getOffset=function(){return this.offset},e.prototype.getCssClassSuffixes=function(){return[this.getType()]},e.prototype.buildTag=function(){return this.tagBuilder.build(this)},e}(),p=function(e,t){return p=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},p(e,t)};function f(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}p(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}var h=function(){return h=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function u(e,t,r){for(var o,a,i=[],s=t;ss&&(n=s-l),a=n;a>=0;a--){let n=!0;for(let r=0;r>>=0,isFinite(n)?(n>>>=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}const o=this.length-t;if((void 0===n||n>o)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");let a=!1;for(;;)switch(r){case"hex":return w(this,e,t,n);case"utf8":case"utf-8":return E(this,e,t,n);case"ascii":case"latin1":case"binary":return x(this,e,t,n);case"base64":return _(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,n);default:if(a)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),a=!0}},l.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const k=4096;function O(e,t,n){let r="";n=Math.min(e.length,n);for(let o=t;o{"use strict";var r=n(62435),o=n(85803),a=n(48219),i=RangeError;e.exports=function(e){var t=o(a(this)),n="",s=r(e);if(s<0||s==1/0)throw i("Wrong number of repetitions");for(;s>0;(s>>>=1)&&(t+=t))1&s&&(n+=t);return n}},93093:(e,t,n)=>{var r=n(79417).PROPER,o=n(95981),a=n(73483);e.exports=function(e){return o((function(){return!!a[e]()||"
"!=="
"[e]()||r&&a[e].name!==e}))}},74853:(e,t,n)=>{var r=n(95329),o=n(48219),a=n(85803),i=n(73483),s=r("".replace),l=RegExp("^["+i+"]+"),u=RegExp("(^|[^"+i+"])["+i+"]+$"),c=function(e){return function(t){var n=a(o(t));return 1&e&&(n=s(n,l,"")),2&e&&(n=s(n,u,"$1")),n}};e.exports={start:c(1),end:c(2),trim:c(3)}},63405:(e,t,n)=>{var r=n(53385),o=n(95981);e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},29630:(e,t,n)=>{var r=n(78834),o=n(626),a=n(99813),i=n(95929);e.exports=function(){var e=o("Symbol"),t=e&&e.prototype,n=t&&t.valueOf,s=a("toPrimitive");t&&!t[s]&&i(t,s,(function(e){return r(n,this)}),{arity:1})}},34680:(e,t,n)=>{var r=n(63405);e.exports=r&&!!Symbol.for&&!!Symbol.keyFor},42941:(e,t,n)=>{var r,o,a,i,s=n(21899),l=n(79730),u=n(86843),c=n(57475),p=n(90953),f=n(95981),h=n(15463),d=n(93765),m=n(61333),g=n(18348),y=n(22749),v=n(6049),b=s.setImmediate,w=s.clearImmediate,E=s.process,x=s.Dispatch,_=s.Function,S=s.MessageChannel,A=s.String,C=0,k={},O="onreadystatechange";f((function(){r=s.location}));var j=function(e){if(p(k,e)){var t=k[e];delete k[e],t()}},T=function(e){return function(){j(e)}},I=function(e){j(e.data)},N=function(e){s.postMessage(A(e),r.protocol+"//"+r.host)};b&&w||(b=function(e){g(arguments.length,1);var t=c(e)?e:_(e),n=d(arguments,1);return k[++C]=function(){l(t,void 0,n)},o(C),C},w=function(e){delete k[e]},v?o=function(e){E.nextTick(T(e))}:x&&x.now?o=function(e){x.now(T(e))}:S&&!y?(i=(a=new S).port2,a.port1.onmessage=I,o=u(i.postMessage,i)):s.addEventListener&&c(s.postMessage)&&!s.importScripts&&r&&"file:"!==r.protocol&&!f(N)?(o=N,s.addEventListener("message",I,!1)):o=O in m("script")?function(e){h.appendChild(m("script"))[O]=function(){h.removeChild(this),j(e)}}:function(e){setTimeout(T(e),0)}),e.exports={set:b,clear:w}},59413:(e,t,n)=>{var r=n(62435),o=Math.max,a=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):a(n,t)}},74529:(e,t,n)=>{var r=n(37026),o=n(48219);e.exports=function(e){return r(o(e))}},62435:(e,t,n)=>{var r=n(35331);e.exports=function(e){var t=+e;return t!=t||0===t?0:r(t)}},43057:(e,t,n)=>{var r=n(62435),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},89678:(e,t,n)=>{var r=n(48219),o=Object;e.exports=function(e){return o(r(e))}},46935:(e,t,n)=>{var r=n(78834),o=n(10941),a=n(56664),i=n(14229),s=n(39811),l=n(99813),u=TypeError,c=l("toPrimitive");e.exports=function(e,t){if(!o(e)||a(e))return e;var n,l=i(e,c);if(l){if(void 0===t&&(t="default"),n=r(l,e,t),!o(n)||a(n))return n;throw u("Can't convert object to primitive value")}return void 0===t&&(t="number"),s(e,t)}},83894:(e,t,n)=>{var r=n(46935),o=n(56664);e.exports=function(e){var t=r(e,"string");return o(t)?t:t+""}},22885:(e,t,n)=>{var r={};r[n(99813)("toStringTag")]="z",e.exports="[object z]"===String(r)},85803:(e,t,n)=>{var r=n(9697),o=String;e.exports=function(e){if("Symbol"===r(e))throw TypeError("Cannot convert a Symbol value to a string");return o(e)}},69826:e=>{var t=String;e.exports=function(e){try{return t(e)}catch(e){return"Object"}}},99418:(e,t,n)=>{var r=n(95329),o=0,a=Math.random(),i=r(1..toString);e.exports=function(e){return"Symbol("+(void 0===e?"":e)+")_"+i(++o+a,36)}},14766:(e,t,n)=>{var r=n(95981),o=n(99813),a=n(82529),i=o("iterator");e.exports=!r((function(){var e=new URL("b?a=1&b=2&c=3","http://a"),t=e.searchParams,n="";return e.pathname="c%20d",t.forEach((function(e,r){t.delete("b"),n+=r+e})),a&&!e.toJSON||!t.sort||"http://a/c%20d?a=1&c=3"!==e.href||"3"!==t.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!t[i]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("http://тест").host||"#%D0%B1"!==new URL("http://a#б").hash||"a1c3"!==n||"x"!==new URL("http://x",void 0).host}))},32302:(e,t,n)=>{var r=n(63405);e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},83937:(e,t,n)=>{var r=n(55746),o=n(95981);e.exports=r&&o((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},18348:e=>{var t=TypeError;e.exports=function(e,n){if(e
/g,"\n"))},"after:highlightElement":({result:e})=>{p.useBR&&(e.value=e.value.replace(/\n/g,"
"))}},y=/^(<[^>]+>|\t)+/gm,v={"after:highlightElement":({result:e})=>{p.tabReplace&&(e.value=e.value.replace(y,(e=>e.replace(/\t/g,p.tabReplace))))}};function b(e){let t=null;const n=function(e){let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"";const n=p.languageDetectRe.exec(t);if(n){const t=_(n[1]);return t||(Q(l.replace("{}",n[1])),Q("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}return t.split(/\s+/).find((e=>f(e)||_(e)))}(e);if(f(n))return;C("before:highlightElement",{el:e,language:n}),t=e;const o=t.textContent,a=n?h(o,{language:n,ignoreIllegals:!0}):m(o);C("after:highlightElement",{el:e,result:a,text:o}),e.innerHTML=a.value,function(e,t,n){const o=t?r[t]:n;e.classList.add("hljs"),o&&e.classList.add(o)}(e,n,a.language),e.result={language:a.language,re:a.relevance,relavance:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance,relavance:a.second_best.relevance})}const w=()=>{if(w.called)return;w.called=!0,X("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(b)};let E=!1;function x(){if("loading"===document.readyState)return void(E=!0);document.querySelectorAll("pre code").forEach(b)}function _(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]}function S(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{r[e.toLowerCase()]=t}))}function A(e){const t=_(e);return t&&!t.disableAutodetect}function C(e,t){const n=e;a.forEach((function(e){e[n]&&e[n](t)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function(){E&&x()}),!1),Object.assign(e,{highlight:h,highlightAuto:m,highlightAll:x,fixMarkup:function(e){return X("10.2.0","fixMarkup will be removed entirely in v11.0"),X("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),t=e,p.tabReplace||p.useBR?t.replace(s,(e=>"\n"===e?p.useBR?"
":e:p.tabReplace?e.replace(/\t/g,p.tabReplace):e)):t;var t},highlightElement:b,highlightBlock:function(e){return X("10.7.0","highlightBlock will be removed entirely in v12.0"),X("10.7.0","Please use highlightElement now."),b(e)},configure:function(e){e.useBR&&(X("10.3.0","'useBR' will be removed entirely in v11.0"),X("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),p=te(p,e)},initHighlighting:w,initHighlightingOnLoad:function(){X("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),E=!0},registerLanguage:function(n,r){let o=null;try{o=r(e)}catch(e){if(Y("Language definition for '{}' could not be registered.".replace("{}",n)),!i)throw e;Y(e),o=u}o.name||(o.name=n),t[n]=o,o.rawDefinition=r.bind(null,e),o.aliases&&S(o.aliases,{languageName:n})},unregisterLanguage:function(e){delete t[e];for(const t of Object.keys(r))r[t]===e&&delete r[t]},listLanguages:function(){return Object.keys(t)},getLanguage:_,registerAliases:S,requireLanguage:function(e){X("10.4.0","requireLanguage will be removed entirely in v11."),X("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const t=_(e);if(t)return t;throw new Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:A,inherit:te,addPlugin:function(e){!function(e){e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{e["before:highlightBlock"](Object.assign({block:t.el},t))}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{e["after:highlightBlock"](Object.assign({block:t.el},t))})}(e),a.push(e)},vuePlugin:W(e).VuePlugin}),e.debugMode=function(){i=!1},e.safeMode=function(){i=!0},e.versionString="10.7.3";for(const e in R)"object"==typeof R[e]&&n(R[e]);return Object.assign(e,R),e.addPlugin(g),e.addPlugin(J),e.addPlugin(v),e}({});e.exports=re},61519:e=>{function t(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const n={},r={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[n]}]};Object.assign(n,{className:"variable",variants:[{begin:t(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},r]});const o={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},a={begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},i={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n,o]};o.contains.push(i);const s={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,n]},l=e.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),u={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[l,e.SHEBANG(),u,s,e.HASH_COMMENT_MODE,a,i,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},n]}}},30786:e=>{function t(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const n="HTTP/(2|1\\.[01])",r={className:"attribute",begin:t("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},o=[r,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+n+" \\d{3})",end:/$/,contains:[{className:"meta",begin:n},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:o}},{begin:"(?=^[A-Z]+ (.*?) "+n+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:n},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:o}},e.inherit(r,{relevance:0})]}}},96344:e=>{const t="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],r=["true","false","null","undefined","NaN","Infinity"],o=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function a(e){return i("(?=",e,")")}function i(...e){return e.map((e=>{return(t=e)?"string"==typeof t?t:t.source:null;var t})).join("")}e.exports=function(e){const s=t,l="<>",u=">",c={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,t)=>{const n=e[0].length+e.index,r=e.input[n];"<"!==r?">"===r&&(((e,{after:t})=>{const n=""+e[0].slice(1);return-1!==e.input.indexOf(n,t)})(e,{after:n})||t.ignoreMatch()):t.ignoreMatch()}},p={$pattern:t,keyword:n,literal:r,built_in:o},f="[0-9](_?[0-9])*",h=`\\.(${f})`,d="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",m={className:"number",variants:[{begin:`(\\b(${d})((${h})|\\.)?|(${h}))[eE][+-]?(${f})\\b`},{begin:`\\b(${d})\\b((${h})\\b|\\.)?|(${h})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},g={className:"subst",begin:"\\$\\{",end:"\\}",keywords:p,contains:[]},y={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,g],subLanguage:"xml"}},v={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,g],subLanguage:"css"}},b={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,g]},w={className:"comment",variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:s+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]},E=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,y,v,b,m,e.REGEXP_MODE];g.contains=E.concat({begin:/\{/,end:/\}/,keywords:p,contains:["self"].concat(E)});const x=[].concat(w,g.contains),_=x.concat([{begin:/\(/,end:/\)/,keywords:p,contains:["self"].concat(x)}]),S={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:p,contains:_};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:p,exports:{PARAMS_CONTAINS:_},illegal:/#(?![$_A-z])/,contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,y,v,b,w,m,{begin:i(/[{,\n]\s*/,a(i(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,s+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:s+a("\\s*:"),relevance:0}]},{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[w,e.REGEXP_MODE,{className:"function",begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:p,contains:_}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:l,end:u},{begin:c.begin,"on:begin":c.isTrulyOpeningTag,end:c.end}],subLanguage:"xml",contains:[{begin:c.begin,end:c.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:p,contains:["self",e.inherit(e.TITLE_MODE,{begin:s}),S],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[S,e.inherit(e.TITLE_MODE,{begin:s})]},{variants:[{begin:"\\."+s},{begin:"\\$"+s}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},e.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[e.inherit(e.TITLE_MODE,{begin:s}),"self",S]},{begin:"(get|set)\\s+(?="+s+"\\()",end:/\{/,keywords:"get set",contains:[e.inherit(e.TITLE_MODE,{begin:s}),{begin:/\(\)/},S]},{begin:/\$[(.]/}]}}},82026:e=>{e.exports=function(e){const t={literal:"true false null"},n=[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],r=[e.QUOTE_STRING_MODE,e.C_NUMBER_MODE],o={end:",",endsWithParent:!0,excludeEnd:!0,contains:r,keywords:t},a={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE],illegal:"\\n"},e.inherit(o,{begin:/:/})].concat(n),illegal:"\\S"},i={begin:"\\[",end:"\\]",contains:[e.inherit(o)],illegal:"\\S"};return r.push(a,i),n.forEach((function(e){r.push(e)})),{name:"JSON",contains:r,keywords:t,illegal:"\\S"}}},66336:e=>{e.exports=function(e){const t={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},n={begin:"`[\\s\\S]",relevance:0},r={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},o={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[n,r,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},a={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},i=e.inherit(e.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),s={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},l={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[e.TITLE_MODE]},u={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[r]}]},c={begin:/using\s/,end:/$/,returnBegin:!0,contains:[o,a,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},p={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},f={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(t.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},e.inherit(e.TITLE_MODE,{endsParent:!0})]},h=[f,i,n,e.NUMBER_MODE,o,a,s,r,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],d={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",h,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return f.contains.unshift(d),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:t,contains:h.concat(l,u,c,p,d)}}},42157:e=>{function t(e){return e?"string"==typeof e?e:e.source:null}function n(e){return r("(?=",e,")")}function r(...e){return e.map((e=>t(e))).join("")}function o(...e){return"("+e.map((e=>t(e))).join("|")+")"}e.exports=function(e){const t=r(/[A-Z_]/,r("(",/[A-Z0-9_.-]*:/,")?"),/[A-Z0-9_.-]*/),a={className:"symbol",begin:/&[a-z]+;|[0-9]+;|[a-f0-9]+;/},i={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},s=e.inherit(i,{begin:/\(/,end:/\)/}),l=e.inherit(e.APOS_STRING_MODE,{className:"meta-string"}),u=e.inherit(e.QUOTE_STRING_MODE,{className:"meta-string"}),c={endsWithParent:!0,illegal:/,relevance:0,contains:[{className:"attr",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[a]},{begin:/'/,end:/'/,contains:[a]},{begin:/[^\s"'=<>`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,u,l,s,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[i,s,u,l]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},a,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/