Repository: OWASP/threat-dragon (js)

Issue:
multiple-line description support 
**Describe the bug**
<!-- Describe below what the problem is -->
If we have written a multi-line `High level system description`, it will be rendered on a single line in the model and report screens.

**Expected behaviour**
<!-- Describe below what you expected to happen -->
To be rendered in multiple lines in the above screens

**Environment**
 - Version: 2.0.0 (e.g. 1.3.1)
 - Platform: Web App (Web App / Desktop App)
 - OS: MacOS (MacOS / Windows / Linux)
 - Browser: chrome (e.g. chrome, firefox, safari)

**To Reproduce**
<!-- List below the steps to reproduce the behaviour -->
- create new model
- write description
- display dashboard & 

**Any additional context, screenshots, etc**
<!-- Add below any other context or screenshots about this bug -->
- submit mutiple-line description
  - ![image](https://user-images.githubusercontent.com/25426601/182982979-d1133484-eacc-49f7-a8ed-26793b047411.png)
- rendering single-line
  - ![image](https://user-images.githubusercontent.com/25426601/182983712-762588f1-3408-43ee-b422-0d3c6147e94f.png)


Pull request description:
fix: multi-line description
**Summary**
<!--
What existing issue does the pull request solve?
Please provide enough information so that others can review your pull request
-->
Fixed an issue where the Description item did not support multi-line display.

**Description for the changelog**
<!--
A short (one line) summary that describes the changes in this
pull request for inclusion in the change log
If this is a bug fix, your description sahould include "fixes #xxxx", or
"closes #xxxx", where #xxxx is the issue number
-->
multi-line rendering of description. fixes #468 

**Other info**
<!--
Thanks for submitting a pull request!
Please make sure you read our contributing guidelines;
https://github.com/OWASP/threat-dragon/blob/main/CONTRIBUTING.md
-->


Patch:
diff --git a/.gitignore b/.gitignore
index ea7fb400..02d6252a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@ installers/
 # ignore Apple's desktop store files
 **/.DS_Store
 # ignore VS and Eclipse files
-.project
 .vscode/
 .settings/
 # ignore IntelliJ IDEA files
diff --git a/docs/README.md b/docs/README.md
index ffaa20e9..06790105 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,10 +7,10 @@ The theme provides the header bar for the documentation site (such as flask, clo
 
 ### Running Locally
 * From a terminal navigate to this `docs` directory
-* Install/update bundle: `npm add --location=global bundle`
+* Install/update bundle: `pnpm add -g bundle`
 * Install jekyll, webrick etc: `bundle update`
 * Run the docs server: `bundle exec jekyll serve`
 * Navigate in a browser to server address: http://127.0.0.1:4000/
 
 
-If you do not already have Ruby installed, see [Jekyll's Guide](https://jekyllrb.com/docs/installation/)
+If you do not already have Ruby installed, see [Jekyll's Guide](https://jekyllrb.com/docs/installation/)
\ No newline at end of file
diff --git a/docs/development/env.md b/docs/development/env.md
index 59318eb9..87ee0d21 100644
--- a/docs/development/env.md
+++ b/docs/development/env.md
@@ -166,10 +166,8 @@ ___
 | Threat Dragon general variables | Description | Default |
 | --- | ----------- | ------- |
 | ENV_FILE | The location of a dotenv file, if dotenv is used. Exported as it needs to be accessed before the dotenv file is read | `.env` |
-| LOG_LEVEL | The server logging level: `audit` / `error` / `warn` / `info` / `debug` / `silly` | `info` |
-| LOG_MAX_FILE_SIZE | Maximum size of the back-end express server log file, in MB | `24` |
 | NODE_ENV | The node environment, typically `test`, `production` or `development`.  The 'secure' cookie flag is set only if running in `production` mode | |
-| PORT | Defines the listening port for Threat Dragon's server, and used by Heroku | `3000`|
+| PORT | Defines the listening port for Threat Dragon's server, used by Heroku | `3000`|
 | SERVER_API_PROTOCOL | The protocol used between Threat Dragon's server and frontend, used by Heroku (http/https) | `http` |
 
 {:.table .table-striped}
@@ -181,6 +179,8 @@ ___
 | GITHUB_CLIENT_ID | The client_id value for the GitHub OAuth app used for authentication | |
 | GITHUB_CLIENT_SECRET | The client_secret generated for the GitHub OAuth app used for authentication | |
 | GITHUB_SCOPE | Defines the github scope: `repo` to access both private and public repos or `public_repo` to access public repos only | `public_repo` |
+| LOG_LEVEL | The server logging level: `audit` / `error` / `warn` / `info` / `debug` / `silly` | `info` |
+| LOG_MAX_FILE_SIZE | Maximum size of the back-end express server log file, in MB | `24` |
 
 **Note:** the JWT refresh signing key should be different from the JWT signing key as they are different tokens.
 A JWT is used as a refresh token because it is tamper resistant and provides user context.
diff --git a/td.server/dev.js b/td.server/dev.js
index 92f4cde8..9715de14 100644
--- a/td.server/dev.js
+++ b/td.server/dev.js
@@ -3,7 +3,7 @@ var appFactory = require('./src/app.js');
 var app = appFactory.default.create();
 
 var server = app.listen(app.get('port'), function() {
-    console.log('Development server listening at ' + server.address().address + ' on ' +  server.address().port);
+    console.log('Express server listening on ' + server.address().address + ':' +  server.address().port);
 });
 
 process.once('SIGUSR2', 
diff --git a/td.server/index.js b/td.server/index.js
index 405bfa2e..374cd55b 100644
--- a/td.server/index.js
+++ b/td.server/index.js
@@ -3,5 +3,5 @@ var appFactory = require('./dist/app.js');
 var app = appFactory.default.create();
 
 var server = app.listen(app.get('port'), function() {
-    console.log('Express server listening at ' + server.address().address + ' on ' +  server.address().port);
+    console.log('Express server listening on ' + server.address().address + ':' +  server.address().port);
 });
diff --git a/td.server/src/app.js b/td.server/src/app.js
index 15f8d842..c84d7990 100644
--- a/td.server/src/app.js
+++ b/td.server/src/app.js
@@ -27,19 +27,12 @@ const create = () => {
     let logger;
 
     try {
-        // logging environment
-        envConfig.tryLoadDotEnv();
-        loggerHelper.level(env.get().config.LOG_LEVEL || 'info');
-        logger = loggerHelper.get('app.js');
-
         const app = expressHelper.getInstance();
         app.set('trust proxy', true);
-        // rate limiting only for production environemnts, otherwise automated e2e tests fail
-        if (process.env.NODE_ENV === 'production') {
-            app.use(limiter);
-        } else {
-            logger.warn('Rate limiting only when running in production environments');
-        }
+
+        // environment configuration
+        envConfig.tryLoadDotEnv();
+        logger = loggerHelper.get('app.js');
 
         //security headers
         securityHeaders.config(app);
@@ -57,11 +50,14 @@ const create = () => {
         //routes
         routes.config(app);
 
+        // rate limiting for the routes
+        app.use(limiter);
+
+        logger.info('OWASP Threat Dragon application started');
+
         // if this default is changed then ensure docs are updated and CI pipeline ci.yaml still works
         app.set('port', env.get().config.PORT || 3000);
-        logger.info('Express server listening on ' + app.get('port'));
 
-        logger.info('OWASP Threat Dragon application started');
         return app;
     }
     catch (e) {
diff --git a/td.server/src/config/bearer.config.js b/td.server/src/config/bearer.config.js
index 1648ba7e..09940406 100644
--- a/td.server/src/config/bearer.config.js
+++ b/td.server/src/config/bearer.config.js
@@ -3,7 +3,6 @@ import jwt from '../helpers/jwt.helper.js';
 import loggerHelper from '../helpers/logger.helper.js';
 
 const logger = loggerHelper.get('config/bearer.config.js');
-logger.silly('Bearer config imported');
 
 /**
  * Extracts the bearer token from the auth header
diff --git a/td.server/src/controllers/auth.js b/td.server/src/controllers/auth.js
index 0b56f257..d553449f 100644
--- a/td.server/src/controllers/auth.js
+++ b/td.server/src/controllers/auth.js
@@ -7,11 +7,8 @@ import responseWrapper from './responseWrapper.js';
 import tokenRepo from '../repositories/token.js';
 
 const logger = loggerHelper.get('controllers/auth.js');
-logger.silly('Auth controller imported');
 
 const login = (req, res) => {
-    logger.debug('API login request:', req);
-
     try {
         const provider = providers.get(req.params.provider);
         return responseWrapper.sendResponse(() => provider.getOauthRedirectUrl(), req, res, logger);
@@ -20,10 +17,7 @@ const login = (req, res) => {
     }
 };
 
-
 const oauthReturn = (req, res) => {
-    logger.debug('API oauthReturn request:', req);
-
     let returnUrl = `/#/oauth-return?code=${req.query.code}`;
     if (env.get().config.NODE_ENV === 'development') {
         returnUrl = `http://localhost:8080${returnUrl}`;
@@ -31,10 +25,7 @@ const oauthReturn = (req, res) => {
     return res.redirect(returnUrl);
 };
 
-
 const completeLogin = (req, res) => {
-    logger.debug('API completeLogin request:', req);
-
     try {
         const provider = providers.get(req.params.provider);
 
@@ -53,10 +44,7 @@ const completeLogin = (req, res) => {
     }
 };
 
-
 const logout = (req, res) => responseWrapper.sendResponse(() => {
-    logger.debug('API logout request:', req);
-
     try {
         const refreshToken = req.body.refreshToken;
         if (!refreshToken) {
@@ -75,10 +63,7 @@ const logout = (req, res) => responseWrapper.sendResponse(() => {
     }
 }, req, res, logger);
 
-
 const refresh = (req, res) => {
-    logger.debug('API refresh request:', req);
-
     const tokenBody = tokenRepo.verify(req.body.refreshToken);
     if (!tokenBody) {
         return errors.unauthorized(res, logger);
@@ -92,7 +77,6 @@ const refresh = (req, res) => {
     }, req, res, logger);
 };
 
-
 export default {
     completeLogin,
     login,
diff --git a/td.server/src/controllers/healthcheck.js b/td.server/src/controllers/healthcheck.js
index acd48c5c..45a7f268 100644
--- a/td.server/src/controllers/healthcheck.js
+++ b/td.server/src/controllers/healthcheck.js
@@ -1,11 +1,4 @@
-import loggerHelper from '../helpers/logger.helper.js';
-
-const logger = loggerHelper.get('controllers/healthcheck.js');
-
-const healthz = (req, res) => {
-    logger.debug('API healthz request:', req);
-    return res.send(true);
-};
+const healthz = (req, res) => res.send(true);
 
 export default {
     healthz
diff --git a/td.server/src/controllers/homecontroller.js b/td.server/src/controllers/homecontroller.js
index e177c94b..5223f202 100644
--- a/td.server/src/controllers/homecontroller.js
+++ b/td.server/src/controllers/homecontroller.js
@@ -1,9 +1,5 @@
-import loggerHelper from '../helpers/logger.helper.js';
 import path from 'path';
 
-const logger = loggerHelper.get('controllers/homecontroller.js');
-logger.silly('Home controller imported');
-
 import { upDir } from '../helpers/path.helper.js';
 
 /**
@@ -19,11 +15,9 @@ const indexHtmlPath = path.join(__dirname, upDir, upDir, upDir, 'dist', 'index.h
  * @returns {Object}
  */
 const index = (req, res) => {
-    logger.debug('API index request:', req);
     res.sendFile(indexHtmlPath);
 };
 
-
 export default {
     index
 };
diff --git a/td.server/src/controllers/threatmodelcontroller.js b/td.server/src/controllers/threatmodelcontroller.js
index 58230890..d5d48a5e 100644
--- a/td.server/src/controllers/threatmodelcontroller.js
+++ b/td.server/src/controllers/threatmodelcontroller.js
@@ -4,14 +4,12 @@ import responseWrapper from './responseWrapper.js';
 import { serverError } from './errors.js';
 
 const logger = loggerHelper.get('controllers/threatmodelcontroller.js');
-logger.silly('ThreatModel controller imported');
 
 const repos = (req, res) => responseWrapper.sendResponseAsync(async () => {
     const page = req.query.page || 1;
     const reposResp = await repository.reposAsync(page, req.provider.access_token);
     const repos = reposResp[0];
     const headers = reposResp[1];
-    logger.debug('API repos request:', req);
 
     return {
         repos: repos.map((x) => x.full_name),
@@ -26,7 +24,6 @@ const branches = (req, res) => responseWrapper.sendResponseAsync(async () => {
         repo: req.params.repo,
         page: req.query.page || 1
     };
-    logger.debug('API branches request:', req);
 
     const branchesResp = await repository.branchesAsync(repoInfo, req.provider.access_token);
     const branches = branchesResp[0],
@@ -39,15 +36,12 @@ const branches = (req, res) => responseWrapper.sendResponseAsync(async () => {
     };
 }, req, res, logger);
 
-
 const models = (req, res) => responseWrapper.sendResponseAsync(async () => {
     const branchInfo = {
         organisation: req.params.organisation,
         repo: req.params.repo,
         branch: req.params.branch
     };
-    logger.debug('API models request:', req);
-
     let modelsResp;
     try {
         modelsResp = await repository.modelsAsync(branchInfo, req.provider.access_token);
@@ -61,7 +55,6 @@ const models = (req, res) => responseWrapper.sendResponseAsync(async () => {
     return modelsResp[0].map((x) => x.name);
 }, req, res, logger);
 
-
 const model = (req, res) => responseWrapper.sendResponseAsync(async () => {
     const modelInfo = {
         organisation: req.params.organisation,
@@ -69,53 +62,46 @@ const model = (req, res) => responseWrapper.sendResponseAsync(async () => {
         branch: req.params.branch,
         model: req.params.model
     };
-    logger.debug('API model request:', req);
-
     const modelResp = await repository.modelAsync(modelInfo, req.provider.access_token);
     return JSON.parse(Buffer.from(modelResp[0].content, 'base64').toString('utf8'));
 }, req, res, logger);
 
-
 const create = async (req, res) => {
-    const modelBody = {
+    const modelInfo = {
         organisation: req.params.organisation,
         repo: req.params.repo,
         branch: req.params.branch,
         model: req.params.model,
         body: req.body        
     };
-    logger.debug('API create request:', req);
 
     try {
-        const createResp = await repository.createAsync(modelBody, req.provider.access_token);
-        return res.status(201).send(createResp);
+        const modelResp = await repository.createAsync(modelInfo, req.provider.access_token);
+        return res.status(201).send(modelResp);
     } catch (err) {
         logger.error(err);
         return serverError('Error creating model', res, logger);
     }
 };
 
-
 const update = async (req, res) => {
-    const modelBody = {
+    const modelInfo = {
         organisation: req.params.organisation,
         repo: req.params.repo,
         branch: req.params.branch,
         model: req.params.model,
         body: req.body        
     };
-    logger.debug('API update request:', req);
 
     try {
-        const updateResp = await repository.updateAsync(modelBody, req.provider.access_token);
-        return res.send(updateResp);
+        const modelResp = await repository.updateAsync(modelInfo, req.provider.access_token);
+        return res.send(modelResp);
     } catch (err) {
         logger.error(err);
         return serverError('Error updating model', res, logger);
     }
 };
 
-
 const deleteModel = async (req, res) => {
     const modelInfo = {
         organisation: req.params.organisation,
@@ -123,18 +109,16 @@ const deleteModel = async (req, res) => {
         branch: req.params.branch,
         model: req.params.model,      
     };
-    logger.debug('API deleteModel request:', req);
 
     try {
-        const deleteResp = await repository.deleteAsync(modelInfo, req.provider.access_token);
-        return res.send(deleteResp);
+        const delResp = await repository.deleteAsync(modelInfo, req.provider.access_token);
+        return res.send(delResp);
     } catch (err) {
         logger.error(err);
         return serverError('Error deleting model', res, logger);
     }
 };
 
-
 const getPagination = (headers, page) => {
     const pagination = { page: page, next: false, prev: false };
     const linkHeader = headers.link;
@@ -156,7 +140,6 @@ const getPagination = (headers, page) => {
     return pagination;  
 };
 
-
 export default {
     branches,
     create,
diff --git a/td.server/src/env/Env.js b/td.server/src/env/Env.js
index be160d12..7a990683 100644
--- a/td.server/src/env/Env.js
+++ b/td.server/src/env/Env.js
@@ -91,11 +91,12 @@ export class Env {
      */
     _loadConfig() {
         const config = {};
+        console.log(`Initializing env config for ${this.name}`);
         this.properties.forEach(({ key, required, defaultValue }) => {
             const prop = `${this.prefix}${key}`;
             const value = process.env[prop] || this.tryReadFromFile(prop) || defaultValue;
             if (!value && required) {
-                const errMsg = `${prop} is a required property, Threat Dragon server cannot start without it. Please see docs env.md for more information`;
+                const errMsg = `${prop} is a required property.  Threat Dragon server cannot start without it.  Please see setup-env.md for more information`;
                 console.error(errMsg);
                 throw new Error(errMsg);
             }
@@ -121,7 +122,6 @@ export class Env {
             dotenv.config({
                 path: envFilePath
             });
-            console.log(`Using config file: ${envFilePath}`);
         } else {
             console.log(`Unable to find .env file, falling back to environment variables`);
         }
diff --git a/td.server/src/helpers/encryption.helper.js b/td.server/src/helpers/encryption.helper.js
index 39569b3d..17d46bd8 100644
--- a/td.server/src/helpers/encryption.helper.js
+++ b/td.server/src/helpers/encryption.helper.js
@@ -5,7 +5,6 @@ import env from '../env/Env.js';
 import loggerHelper from './logger.helper.js';
 
 const logger = loggerHelper.get('helpers/encryption.helper.js');
-logger.silly('Encryption helper imported');
 
 const inputEncoding = 'ascii';
 const outputEncoding = 'base64';
diff --git a/td.server/src/helpers/logger.helper.js b/td.server/src/helpers/logger.helper.js
index 92ed66c0..8cb2a42e 100644
--- a/td.server/src/helpers/logger.helper.js
+++ b/td.server/src/helpers/logger.helper.js
@@ -1,9 +1,10 @@
 /**
  * @name logger.js
  * @description Default logging implementation
- * logging level either provided by dotenv LOG_LEVEL or defaults to 'info'
  */
-import winston, { format } from 'winston';
+import winston, { format, transports } from 'winston';
+
+import env from '../env/Env.js';
 
 /**
  * The available log levels
@@ -18,29 +19,9 @@ const logLevels = {
     silly: 5
 };
 
-// declare the various destinations for the logging messages
-const transports = {
-    app: new winston.transports.File({
-            filename: 'app.log',
-            level: 'info',
-            silent: process.env.NODE_ENV === 'test'
-        }),
-    audit: new winston.transports.File({
-            filename: 'audit.log',
-            level: 'audit',
-            silent: process.env.NODE_ENV === 'test'
-        }),
-    console: new winston.transports.Console({
-            format: format.combine(format.colorize(), format.simple()),
-            level: 'info',
-            silent: process.env.NODE_ENV === 'test'
-        })
-};
-
-
 const _logger = winston.createLogger({
     levels: logLevels,
-    level: 'info',
+    level: env.get().config.LOG_LEVEL || 'info',
     format: format.combine(
         format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
         format.errors({ stack: true }),
@@ -49,14 +30,25 @@ const _logger = winston.createLogger({
     ),
     defaultMeta: { service: 'threat-dragon' },
     transports: [
-        transports.audit,
-        transports.app,
-        transports.console
+        new transports.File({
+            filename: 'audit.log',
+            level: 'audit',
+            silent: process.env.NODE_ENV === 'test'
+        }),
+        new transports.File({
+            filename: 'app.log',
+            level: env.get().config.LOG_LEVEL || 'info',
+            silent: process.env.NODE_ENV === 'test'
+        }),
+        new transports.Console({
+            format: format.combine(format.colorize(), format.simple()),
+            level: env.get().config.LOG_LEVEL || 'info',
+            silent: process.env.NODE_ENV === 'test'
+        })
     ],
     silent: process.env.NODE_ENV === 'test'
 });
 
-
 class Logger {
     constructor(service, logger) {
         this.service = service;
@@ -86,7 +78,6 @@ class Logger {
     audit(message) { this.logger.error(this._formatMessage(this.service, message, 'audit')); }
 }
 
-
 /**
  * Gets a new instance of a logger for a given service
  * @param {string} service
@@ -95,10 +86,7 @@ class Logger {
  */
 const get = (service, logger) => new Logger(service, logger);
 
-const level = (logLevel) => { transports.console.level = logLevel; transports.app.level = logLevel; };
-
 export default {
-    level,
     get,
     Logger
 };
diff --git a/td.server/src/providers/index.js b/td.server/src/providers/index.js
index d1d996a8..fdf7543c 100644
--- a/td.server/src/providers/index.js
+++ b/td.server/src/providers/index.js
@@ -23,7 +23,7 @@ const get = (name) => {
     }
 
     if (!provider.isConfigured()) {
-        throw new Error(`Provider ${name} is not configured. See docs env.md for more info`);
+        throw new Error(`Provider ${name} is not configured.  See setup-env.md for more info`);
     }
 
     return provider;
diff --git a/td.server/src/repositories/token.js b/td.server/src/repositories/token.js
index ed9de9dc..716b15a6 100644
--- a/td.server/src/repositories/token.js
+++ b/td.server/src/repositories/token.js
@@ -2,7 +2,6 @@ import jwtHelper from '../helpers/jwt.helper.js';
 import loggerHelper from '../helpers/logger.helper.js';
 
 const logger = loggerHelper.get('repositories/token.js');
-logger.silly('Token repository imported');
 
 /**
  * The refresh tokens
diff --git a/td.vue/src/components/report/ExecutiveSummary.vue b/td.vue/src/components/report/ExecutiveSummary.vue
index 64a2d10d..cbfc7247 100644
--- a/td.vue/src/components/report/ExecutiveSummary.vue
+++ b/td.vue/src/components/report/ExecutiveSummary.vue
@@ -20,6 +20,12 @@
     </div>
 </template>
 
+<style lang="scss" scoped>
+.td-summary {
+    white-space: pre-wrap;
+}
+</style>
+
 <script>
 export default {
     name: 'TdExecutiveSummary',
diff --git a/td.vue/src/views/ThreatModel.vue b/td.vue/src/views/ThreatModel.vue
index 184ac1e3..00eb9e5d 100644
--- a/td.vue/src/views/ThreatModel.vue
+++ b/td.vue/src/views/ThreatModel.vue
@@ -74,6 +74,7 @@
 <style lang="scss" scoped>
 .tm-card {
     font-size: 14px;
+    white-space: pre-wrap;
 }
 .diagram-header-text a {
     color: $black;


Test patch:
diff --git a/td.server/test/mocks/express.mocks.js b/td.server/test/mocks/express.mocks.js
index 60427065..7916cddd 100644
--- a/td.server/test/mocks/express.mocks.js
+++ b/td.server/test/mocks/express.mocks.js
@@ -7,12 +7,10 @@ import sinon from 'sinon';
 
 export const getMockApp = () => {
     const mockApp = {
-        get: () => {},
         set: () => {},
         use: () => {}
     };
 
-    sinon.spy(mockApp, 'get');
     sinon.spy(mockApp, 'set');
     sinon.spy(mockApp, 'use');