Repository: OWASP/threat-dragon
Pull request #1031 (2024-07-21 23:29:42)

Syncing changes with main


Patch:
diff --git a/.github/workflows/browserstack.yaml b/.github/workflows/browserstack.yaml
index 6c5aae02..b319a6e8 100644
--- a/.github/workflows/browserstack.yaml
+++ b/.github/workflows/browserstack.yaml
@@ -20,7 +20,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -31,6 +31,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: |
diff --git a/.github/workflows/housekeeping.yaml b/.github/workflows/housekeeping.yaml
index 06d4bd2c..d99c9fe4 100644
--- a/.github/workflows/housekeeping.yaml
+++ b/.github/workflows/housekeeping.yaml
@@ -65,7 +65,7 @@ jobs:
           ref: main
 
       - name: Run vulnerability scanner
-        uses: aquasecurity/trivy-action@0.23.0
+        uses: aquasecurity/trivy-action@0.24.0
         with:
           image-ref: '${{ env.IMAGE_NAME }}:latest'
           format: 'template'
diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml
index 3896c99a..005798e1 100644
--- a/.github/workflows/pull_request.yaml
+++ b/.github/workflows/pull_request.yaml
@@ -24,7 +24,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -35,6 +35,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -57,7 +58,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -68,6 +69,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -90,7 +92,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -101,6 +103,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -163,10 +166,11 @@ jobs:
           key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
           restore-keys: |
             ${{ runner.os }}-buildx-
+            ${{ runner.os }}-
 
       - name: Build for amd64
         id: docker_build
-        uses: docker/build-push-action@v6.3.0
+        uses: docker/build-push-action@v6.4.1
         with:
           context: ./
           file: ./Dockerfile
@@ -232,7 +236,7 @@ jobs:
             ${{ env.IMAGE_NAME }}
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -243,6 +247,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -383,7 +388,7 @@ jobs:
           docker load --input /tmp/${{ env.IMAGE_NAME }}.tar
 
       - name: Run Trivy vulnerability scanner
-        uses: aquasecurity/trivy-action@0.23.0
+        uses: aquasecurity/trivy-action@0.24.0
         with:
           image-ref: '${{ env.IMAGE_NAME }}'
           format: 'table'
diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml
index b28716ff..224f06f7 100644
--- a/.github/workflows/push.yaml
+++ b/.github/workflows/push.yaml
@@ -27,7 +27,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -38,6 +38,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -69,7 +70,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -80,6 +81,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -102,7 +104,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -113,6 +115,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -163,7 +166,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
+        uses: docker/setup-qemu-action@v3.1.0
 
       - name: Set up Docker Buildx
         id: buildx
@@ -178,6 +181,7 @@ jobs:
           key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
           restore-keys: |
             ${{ runner.os }}-buildx-
+            ${{ runner.os }}-
 
       - name: Login to Docker Hub
         uses: docker/login-action@v3.2.0
@@ -188,7 +192,7 @@ jobs:
       # platform manifests not (yet) supported, so split out architectures
       - name: Build  for amd64 and push latest
         id: docker_build_amd64
-        uses: docker/build-push-action@v6.3.0
+        uses: docker/build-push-action@v6.4.1
         with:
           context: ./
           file: ./Dockerfile
@@ -202,7 +206,7 @@ jobs:
 
       - name: Build for arm64 and push latest-arm64
         id: docker_build_arm64
-        uses: docker/build-push-action@v6.3.0
+        uses: docker/build-push-action@v6.4.1
         with:
           context: ./
           file: ./Dockerfile
@@ -262,7 +266,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -273,6 +277,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -312,7 +317,7 @@ jobs:
             ${{ env.IMAGE_NAME }}
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -323,6 +328,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -363,7 +369,7 @@ jobs:
             ${{ env.IMAGE_NAME }}
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -374,6 +380,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -433,7 +440,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Run Trivy vulnerability scanner
-        uses: aquasecurity/trivy-action@0.23.0
+        uses: aquasecurity/trivy-action@0.24.0
         with:
           image-ref: '${{ env.IMAGE_NAME }}'
           format: 'table'
@@ -452,7 +459,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -463,6 +470,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm install
@@ -490,7 +498,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -501,6 +509,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm install
@@ -530,7 +539,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -541,6 +550,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm install
@@ -575,7 +585,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -586,6 +596,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm install
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 8efacf34..e43e90ea 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -26,7 +26,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -37,6 +37,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -59,7 +60,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -70,6 +71,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -101,7 +103,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -112,6 +114,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install packages
         run: npm clean-install
@@ -136,7 +139,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -147,6 +150,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install clean packages
         run: npm clean-install
@@ -185,7 +189,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -196,6 +200,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install clean packages
         run: npm clean-install
@@ -242,7 +247,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -253,6 +258,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install clean packages
         run: npm clean-install
@@ -290,7 +296,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Use node LTS 20.14.0
-        uses: actions/setup-node@v4.0.2
+        uses: actions/setup-node@v4.0.3
         with:
           node-version: '20.14.0'
 
@@ -301,6 +307,7 @@ jobs:
           key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
           restore-keys: |
             ${{ runner.os }}-node-
+            ${{ runner.os }}-
 
       - name: Install clean packages
         run: npm clean-install
@@ -342,7 +349,7 @@ jobs:
         uses: actions/checkout@v4.1.1
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3
+        uses: docker/setup-qemu-action@v3.1.0
 
       - name: Set up Docker Buildx
         id: buildx
@@ -357,6 +364,7 @@ jobs:
           key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }}
           restore-keys: |
             ${{ runner.os }}-buildx-
+            ${{ runner.os }}-
 
       - name: Login to Docker Hub
         uses: docker/login-action@v3.2.0
@@ -367,7 +375,7 @@ jobs:
       # platform manifests not (yet) supported, so split out architectures
       - name: Build for amd64 and push to Docker Hub
         id: docker_build_amd64
-        uses: docker/build-push-action@v6.3.0
+        uses: docker/build-push-action@v6.4.1
         with:
           context: ./
           file: ./Dockerfile
@@ -381,7 +389,7 @@ jobs:
 
       - name: Build for arm64 and push to Docker Hub
         id: docker_build_arm64
-        uses: docker/build-push-action@v6.3.0
+        uses: docker/build-push-action@v6.4.1
         with:
           context: ./
           file: ./Dockerfile
diff --git a/.gitignore b/.gitignore
index 2516638f..83daaf5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,12 +46,43 @@
 
 # Express server
 !td.server
-!td.server/src/**/*.js
-!td.server/test/**/*.js
-!td.server/**/*.js
-!td.server/**/**/*.js
+!td.server/src/
+!td.server/src/*.js
+!td.server/src/config/
+!td.server/src/config/*.js
+!td.server/src/controllers/
+!td.server/src/controllers/*.js
+!td.server/src/env/
+!td.server/src/env/*.js
+!td.server/src/helpers/
+!td.server/src/helpers/*.js
+!td.server/src/providers/
+!td.server/src/providers/*.js
+!td.server/src/repositories/
+!td.server/src/repositories/*.js
+!td.server/test/
+!td.server/test/*.spec.js
+!td.server/test/config/
+!td.server/test/config/*.spec.js
+!td.server/test/controllers/
+!td.server/test/controllers/*.spec.js
+!td.server/test/env/
+!td.server/test/env/*.spec.js
+!td.server/test/helpers/
+!td.server/test/helpers/*.spec.js
+!td.server/test/mocks/
+!td.server/test/mocks/*mock*js
+!td.server/test/providers/
+!td.server/test/providers/*.spec.js
+!td.server/test/public/
+!td.server/test/public/*.ico
+!td.server/test/repositories/
+!td.server/test/repositories/*.spec.js
 !td.server/.babelrc
+!td.server/.eslintrc.js
 !td.server/README.md
+!td.server/dev.js
+!td.server/index.js
 !td.server/package.json
 !td.server/package-lock.json
 
diff --git a/td.server/src/controllers/configcontroller.js b/td.server/src/controllers/configcontroller.js
index cd450eed..56775366 100644
--- a/td.server/src/controllers/configcontroller.js
+++ b/td.server/src/controllers/configcontroller.js
@@ -4,11 +4,6 @@ import responseWrapper from "./responseWrapper";
 
 const logger = loggerHelper.get('controllers/configcontroller.js');
 
-/**
- * @param {Object} req
- * @param {Object} res
- * @returns {Object}
- */
 const config = (req, res) => responseWrapper.sendResponse(() => getConfig(), req, res, logger);
 
 export const getConfig = () => ({
diff --git a/td.server/src/controllers/threatmodelcontroller.js b/td.server/src/controllers/threatmodelcontroller.js
index 29e9b53b..93b99ecd 100644
--- a/td.server/src/controllers/threatmodelcontroller.js
+++ b/td.server/src/controllers/threatmodelcontroller.js
@@ -12,11 +12,12 @@ const repos = (req, res) => responseWrapper.sendResponseAsync(async () => {
     const page = req.query.page || 1;
     let reposResp;
     let repos;
-    if (env.get().config.REPO_USE_SEARCH === 'true') {
+    // backwardly compatible with previous use of env vars GITHUB_USE_SEARCH and GITHUB_SEARCH_QUERY
+    if (env.get().config.REPO_USE_SEARCH === 'true' || env.get().config.GITHUB_USE_SEARCH === 'true') {
         logger.debug('Using searchAsync');
-        const searchQuery = env.get().config.REPO_SEARCH_QUERY;
+        const searchQuery = env.get().config.REPO_SEARCH_QUERY ?? env.get().config.GITHUB_SEARCH_QUERY;
         reposResp = await repository.searchAsync(page, req.provider.access_token, searchQuery);
-        repos = reposResp[0].items;
+        repos = reposResp[0].items ?? reposResp[0];
     } else {
         logger.debug('Using reposAsync');
         reposResp = await repository.reposAsync(page, req.provider.access_token);
diff --git a/td.server/src/env/Bitbucket.js b/td.server/src/env/Bitbucket.js
index 1bf27aa4..6bcc5e5e 100644
--- a/td.server/src/env/Bitbucket.js
+++ b/td.server/src/env/Bitbucket.js
@@ -14,12 +14,10 @@ class BitbucketEnv extends Env {
         return [
             { key: 'CLIENT_ID', required: false },
             { key: 'CLIENT_SECRET', required: false },
-            { key: 'SCOPE', required: false, defaultValue: 'public_repo' },
+            { key: 'SCOPE', required: false, defaultValue: 'repository:read' },
             { key: 'ENTERPRISE_HOSTNAME', required: false },
             { key: 'ENTERPRISE_PORT', required: false, defaultValue: 443 },
             { key: 'ENTERPRISE_PROTOCOL', required: false, defaultValue: 'https' },
-            { key: 'USE_SEARCH', required: false, defaultValue: false },
-            { key: 'SEARCH_QUERY', required: false },
             { key: 'WORKSPACE', required: false },
             { key: 'REPO_ROOT_DIRECTORY', required: false }
         ];
diff --git a/td.server/src/env/Gitlab.js b/td.server/src/env/Gitlab.js
index 51694626..fb09651f 100644
--- a/td.server/src/env/Gitlab.js
+++ b/td.server/src/env/Gitlab.js
@@ -9,14 +9,13 @@ class GitlabEnv extends Env {
         return 'GITLAB_';
     }
 
+    // Note that the actual env var will be prepended with GITLAB_
     get properties () {
         return [
             { key: 'CLIENT_ID', required: false },
             { key: 'CLIENT_SECRET', required: false },
-            { key: 'SCOPE', required: false },
+            { key: 'SCOPE', required: false, defaultValue: 'read_user read_repository' },
             { key: 'HOST', required: false },
-            { key: 'USE_SEARCH', required: false },
-            { key: 'SEARCH_QUERY', required: false },
             { key: 'REDIRECT_URI', required: false },
             { key: 'REPO_ROOT_DIRECTORY', required: false }
         ];
diff --git a/td.server/src/env/ThreatDragon.js b/td.server/src/env/ThreatDragon.js
index 3edd011d..13087065 100644
--- a/td.server/src/env/ThreatDragon.js
+++ b/td.server/src/env/ThreatDragon.js
@@ -17,7 +17,9 @@ class ThreatDragonEnv extends Env {
             { key: 'LOG_MAX_FILE_SIZE', required: false, defaultValue: 24 },
             { key: 'LOG_LEVEL', required: false, defaultValue: 'warn' },
             { key: 'SERVER_API_PROTOCOL', required: false, defaultValue: 'https' },
-            { key: 'REPO_ROOT_DIRECTORY', required: false, defaultValue: 'ThreatDragonModels' }
+            { key: 'REPO_ROOT_DIRECTORY', required: false, defaultValue: 'ThreatDragonModels' },
+            { key: 'REPO_USE_SEARCH', required: false, defaultValue: false },
+            { key: 'REPO_SEARCH_QUERY', required: false }
         ];
     }
 }
diff --git a/td.vue/package-lock.json b/td.vue/package-lock.json
index 06ac935d..0b519ee4 100644
--- a/td.vue/package-lock.json
+++ b/td.vue/package-lock.json
@@ -20,7 +20,7 @@
         "core-js": "^3.23.3",
         "dotenv": "^8.6.0",
         "electron-log": "^4.4.8",
-        "electron-updater": "^5.3.0",
+        "electron-updater": "^6.3.0",
         "is-electron": "^2.2.1",
         "jquery": "^3.6.3",
         "uuid": "^8.3.2",
@@ -4280,10 +4280,11 @@
       }
     },
     "node_modules/@pm2/agent": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.3.tgz",
-      "integrity": "sha512-xkqqCoTf5VsciMqN0vb9jthW7olVAi4KRFNddCc7ZkeJZ3i8QwZANr4NSH2H5DvseRFHq7MiPspRY/EWAFWWTg==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
+      "integrity": "sha512-n7WYvvTJhHLS2oBb1PjOtgLpMhgImOq8sXkPBw6smeg9LJBWZjiEgPKOpR8mn9UJZsB5P3W4V/MyvNnp31LKeA==",
       "dev": true,
+      "license": "AGPL-3.0",
       "dependencies": {
         "async": "~3.2.0",
         "chalk": "~3.0.0",
@@ -4297,7 +4298,7 @@
         "pm2-axon-rpc": "~0.7.0",
         "proxy-agent": "~6.3.0",
         "semver": "~7.5.0",
-        "ws": "~7.4.0"
+        "ws": "~7.5.10"
       }
     },
     "node_modules/@pm2/agent/node_modules/ansi-styles": {
@@ -4406,27 +4407,6 @@
         "node": ">=8"
       }
     },
-    "node_modules/@pm2/agent/node_modules/ws": {
-      "version": "7.4.6",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
-      "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.3.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@pm2/agent/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -5191,7 +5171,8 @@
     "node_modules/@types/semver": {
       "version": "7.5.8",
       "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
-      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+      "dev": true
     },
     "node_modules/@types/send": {
       "version": "0.17.4",
@@ -14584,19 +14565,19 @@
       "dev": true
     },
     "node_modules/electron-updater": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-5.3.0.tgz",
-      "integrity": "sha512-iKEr7yQBcvnQUPnSDYGSWC9t0eF2YbZWeYYYZzYxdl+HiRejXFENjYMnYjoOm2zxyD6Cr2JTHZhp9pqxiXuCOw==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.0.tgz",
+      "integrity": "sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw==",
+      "license": "MIT",
       "dependencies": {
-        "@types/semver": "^7.3.6",
-        "builder-util-runtime": "9.1.1",
-        "fs-extra": "^10.0.0",
+        "builder-util-runtime": "9.2.5",
+        "fs-extra": "^10.1.0",
         "js-yaml": "^4.1.0",
         "lazy-val": "^1.0.5",
         "lodash.escaperegexp": "^4.1.2",
         "lodash.isequal": "^4.5.0",
-        "semver": "^7.3.5",
-        "typed-emitter": "^2.1.0"
+        "semver": "^7.3.8",
+        "tiny-typed-emitter": "^2.1.0"
       }
     },
     "node_modules/electron-updater/node_modules/argparse": {
@@ -14605,9 +14586,10 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
     "node_modules/electron-updater/node_modules/builder-util-runtime": {
-      "version": "9.1.1",
-      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.1.1.tgz",
-      "integrity": "sha512-azRhYLEoDvRDR8Dhis4JatELC/jUvYjm4cVSj7n9dauGTOM2eeNn9KS0z6YA6oDsjI1xphjNbY6PZZeHPzzqaw==",
+      "version": "9.2.5",
+      "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
+      "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
+      "license": "MIT",
       "dependencies": {
         "debug": "^4.3.4",
         "sax": "^1.2.4"
@@ -28588,7 +28570,7 @@
       "version": "7.8.1",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
       "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
-      "devOptional": true,
+      "dev": true,
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -30891,6 +30873,12 @@
       "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
       "dev": true
     },
+    "node_modules/tiny-typed-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
+      "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
+      "license": "MIT"
+    },
     "node_modules/tmp": {
       "version": "0.2.2",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.2.tgz",
@@ -31082,7 +31070,7 @@
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
-      "devOptional": true
+      "dev": true
     },
     "node_modules/tsutils": {
       "version": "3.21.0",
@@ -31261,14 +31249,6 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/typed-emitter": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz",
-      "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==",
-      "optionalDependencies": {
-        "rxjs": "*"
-      }
-    },
     "node_modules/typedarray-to-buffer": {
       "version": "3.1.5",
       "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -33418,27 +33398,6 @@
         "node": ">=10.18.1"
       }
     },
-    "node_modules/wdio-electron-service/node_modules/devtools/node_modules/ws": {
-      "version": "8.2.3",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
-      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.0.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/wdio-electron-service/node_modules/edge-paths": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-2.2.1.tgz",
@@ -35376,10 +35335,11 @@
       }
     },
     "node_modules/ws": {
-      "version": "7.5.9",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
-      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+      "version": "7.5.10",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
+      "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8.3.0"
       },
diff --git a/td.vue/package.json b/td.vue/package.json
index ebba574c..33eb9d13 100644
--- a/td.vue/package.json
+++ b/td.vue/package.json
@@ -62,7 +62,7 @@
     "core-js": "^3.23.3",
     "dotenv": "^8.6.0",
     "electron-log": "^4.4.8",
-    "electron-updater": "^5.3.0",
+    "electron-updater": "^6.3.0",
     "is-electron": "^2.2.1",
     "jquery": "^3.6.3",
     "uuid": "^8.3.2",
@@ -153,6 +153,7 @@
     "node-notifier@<8.0.1": ">=8.0.1",
     "node-ipc": ">=11.1.0",
     "nth-check@<2.0.1": ">=2.0.1",
+    "@pm2/agent": ">=2.0.4",
     "postcss@<8.4.31": ">=8.4.31",
     "systeminformation@<5.21.8": ">=5.21.8",
     "tar@>=6.0.0 <6.1.9": ">=6.1.9",
@@ -163,6 +164,7 @@
     },
     "webpack@<5.0.0": ">=5.77.0",
     "webpack-dev-middleware": ">=5.3.4",
+    "ws": ">=7.5.10",
     "yargs-parser@>=6.0.0 <13.1.2": ">=13.1.2"
   },
   "main": "background.js",
diff --git a/td.vue/src/components/Graph.vue b/td.vue/src/components/Graph.vue
index a21498f2..3202ef06 100644
--- a/td.vue/src/components/Graph.vue
+++ b/td.vue/src/components/Graph.vue
@@ -24,12 +24,11 @@
                 </b-row>
             </b-col>
         </b-row>
-        <td-graph-meta @threatSelected="threatSelected" @threatSuggest="threatSuggest" />
+        <td-graph-meta @threatSelected="threatSelected" />
 
         <div>
             <td-keyboard-shortcuts />
             <td-threat-edit-dialog ref="threatEditDialog" />
-            <td-threat-suggest-dialog ref="threatSuggestDialog" />
         </div>
     </div>
 </template>
@@ -47,7 +46,6 @@ import TdGraphButtons from '@/components/GraphButtons.vue';
 import TdGraphMeta from '@/components/GraphMeta.vue';
 import TdKeyboardShortcuts from '@/components/KeyboardShortcuts.vue';
 import TdThreatEditDialog from '@/components/ThreatEditDialog.vue';
-import TdThreatSuggestDialog from './ThreatSuggestDialog.vue';
 
 import { getProviderType } from '@/service/provider/providers.js';
 import diagramService from '@/service/migration/diagram.js';
@@ -60,8 +58,7 @@ export default {
         TdGraphButtons,
         TdGraphMeta,
         TdKeyboardShortcuts,
-        TdThreatEditDialog,
-        TdThreatSuggestDialog
+        TdThreatEditDialog
     },
     computed: mapState({
         diagram: (state) => state.threatmodel.selectedDiagram,
@@ -89,9 +86,6 @@ export default {
         threatSelected(threatId,state) {
             this.$refs.threatEditDialog.editThreat(threatId,state);
         },
-        threatSuggest(type){
-            this.$refs.threatSuggestDialog.showModal(type);
-        },
         saved() {
             console.debug('Save diagram');
             const updated = Object.assign({}, this.diagram);
diff --git a/td.vue/src/components/GraphMeta.vue b/td.vue/src/components/GraphMeta.vue
index 135df7e1..d7b772d9 100644
--- a/td.vue/src/components/GraphMeta.vue
+++ b/td.vue/src/components/GraphMeta.vue
@@ -11,6 +11,7 @@
             <b-card header-tag="header">
                 <template #header>
                     {{ $t('threatmodel.threats') }}
+
                     <b-btn
                         :disabled="disableNewThreat"
                         @click="newThreat()"
@@ -51,20 +52,11 @@
                     </b-card-text>
                 </b-card-body>
             </b-card>
-            <a href="javascript:void(0)" v-if="!!cellRef" @click="AddThreatByType()" class="new-threat-by-type m-2">
-                <font-awesome-icon icon="plus"></font-awesome-icon>
-                {{ $t('threats.newThreatByType') }}
-            </a>
         </b-col>
     </b-row>
 </template>
 
 <style lang="scss" scoped>
-.new-threat-by-type {
-    color: $orange;
-    font-size: 16px;
-    padding: 15px;
-}
 .props-header {
     a {
         font-size: 12px;
@@ -128,12 +120,6 @@ export default {
             this.$store.dispatch(CELL_DATA_UPDATED, this.cellRef.data);
             dataChanged.updateStyleAttrs(this.cellRef);
             this.threatSelected(threat.id,'new');
-        },
-        AddThreatByType(){
-            this.$emit('threatSuggest','type');
-        },
-        AddThreatByContext(){
-            this.$emit('threatSuggest','context');
         }
     },
 };
diff --git a/td.vue/src/components/ThreatEditDialog.vue b/td.vue/src/components/ThreatEditDialog.vue
index 2922003e..e29805cc 100644
--- a/td.vue/src/components/ThreatEditDialog.vue
+++ b/td.vue/src/components/ThreatEditDialog.vue
@@ -228,19 +228,19 @@ export default {
         },
         updateThreat() {
             const threatRef = this.cellRef.data.threats.find(x => x.id === this.threat.id);
+            const objRef = this.cellRef.data;
+            if(!objRef.threatFrequency){
+                const tmpfreq = threatModels.getFrequencyMapByElement(this.threat.modelType,this.cellRef.data.type);
+                if(tmpfreq!==null)
+                    objRef.threatFrequency = tmpfreq;
+            }
+            if(objRef.threatFrequency){
+                Object.keys(objRef.threatFrequency).forEach((k)=>{
+                    if(this.$t(`threats.model.${this.threat.modelType.toLowerCase()}.${k}`)===this.threat.type)
+                        objRef.threatFrequency[k]++;
+                });
+            }
             if (threatRef) {
-                const objRef = this.cellRef.data;
-                if(!objRef.threatFrequency){
-                    const tmpfreq = threatModels.getFrequencyMapByElement(this.threat.modelType,this.cellRef.data.type);
-                    if(tmpfreq!==null)
-                        objRef.threatFrequency = tmpfreq;
-                }
-                if(objRef.threatFrequency){
-                    Object.keys(objRef.threatFrequency).forEach((k)=>{
-                        if(this.$t(`threats.model.${this.threat.modelType.toLowerCase()}.${k}`)===this.threat.type)
-                            objRef.threatFrequency[k]++;
-                    });
-                }
                 threatRef.status = this.threat.status;
                 threatRef.severity = this.threat.severity;
                 threatRef.title = this.threat.title;
diff --git a/td.vue/src/components/ThreatSuggestDialog.vue b/td.vue/src/components/ThreatSuggestDialog.vue
deleted file mode 100644
index e7a30c7c..00000000
--- a/td.vue/src/components/ThreatSuggestDialog.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-<template>
-    <div>
-        <b-modal
-            v-if="!!threat"
-            id="threat-edit"
-            size="lg"
-            ok-variant="primary"
-            header-bg-variant="primary"
-            header-text-variant="light"
-            :title="modalTitle"
-            ref="editModal"
-        >
-            <b-form>
-                <b-form-row>
-                    <b-col>
-                        <b-form-group
-                            id="title-group"
-                            :label="$t('threats.properties.title')"
-                            label-for="title">
-                            <b-form-input
-                                id="title"
-                                v-model="threat.title"
-                                type="text"
-                                required
-                            ></b-form-input>
-                        </b-form-group>
-                    </b-col>
-                </b-form-row>
-
-                <b-form-row>
-                    <b-col>
-                        <b-form-group
-                            id="threat-type-group"
-                            :label="$t('threats.properties.type')"
-                            label-for="threat-type">
-                            <b-form-select
-                                id="threat-type"
-                                v-model="threat.type"
-                                :options="threatTypes">
-                            </b-form-select>
-                        </b-form-group>
-                    </b-col>
-                </b-form-row>
-
-                <b-form-row>
-                    <b-col md=5>
-                        <b-form-group
-                            id="status-group"
-                            class="float-left"
-                            :label="$t('threats.properties.status')"
-                            label-for="status">
-                            <b-form-radio-group
-                                id="status"
-                                v-model="threat.status"
-                                :options="statuses"
-                                buttons
-                            ></b-form-radio-group>
-                        </b-form-group>
-                    </b-col>
-
-                    <b-col md=2>
-                        <b-form-group
-                            id="score-group"
-                            :label="$t('threats.properties.score')"
-                            label-for="score">
-                            <b-form-input
-                                id="score"
-                                v-model="threat.score"
-                                type="text"
-                            ></b-form-input>
-                        </b-form-group>
-                    </b-col>
-
-                    <b-col md=5>
-                        <b-form-group
-                            id="priority-group"
-                            class="float-right"
-                            :label="$t('threats.properties.priority')"
-                            label-for="priority">
-                            <b-form-radio-group
-                                id="priority"
-                                v-model="threat.severity"
-                                :options="priorities"
-                                buttons
-                            ></b-form-radio-group>
-                        </b-form-group>
-                    </b-col>
-                </b-form-row>
-
-                <b-form-row>
-                    <b-col>
-                        <b-form-group
-                            id="description-group"
-                            :label="$t('threats.properties.description')"
-                            label-for="description">
-                            <b-form-textarea
-                                id="description"
-                                v-model="threat.description"
-                                rows="5">
-                            </b-form-textarea>
-                        </b-form-group>
-                    </b-col>
-                </b-form-row>
-
-                <b-form-row>
-                    <b-col>
-                        <b-form-group
-                            id="mitigation-group"
-                            :label="$t('threats.properties.mitigation')"
-                            label-for="mitigation">
-                            <b-form-textarea
-                                id="mitigation"
-                                v-model="threat.mitigation"
-                                rows="5">
-                            </b-form-textarea>
-                        </b-form-group>
-                    </b-col>
-                </b-form-row>
-            </b-form>
-
-            <template #modal-footer>
-                <div class="w-100">
-                <b-button
-                    variant="danger"
-                    class="float-right"
-                    @click="acceptSuggestion()"
-                >
-                    {{ $t('forms.apply') }}
-                </b-button>
-                <b-button
-                variant="secondary"
-                class="float-left"
-                @click="previous()"
-                >
-                {{ $t('forms.previous') }}
-            </b-button>
-            <b-button
-                variant="danger"
-                class="float-left"
-                @click="next()"
-            >
-                {{ $t('forms.next') }}
-            </b-button>
-                <b-button
-                variant="secondary"
-                class="float-right mr-2"
-                @click="hideModal()"
-            >
-                {{ $t('forms.cancel') }}
-            </b-button>
-                </div>
-            </template>
-        </b-modal>
-    </div>
-</template>
-
-
-<script>
-import {mapState} from 'vuex';
-import { createNewTypedThreat } from '@/service/threats/index.js';
-import { CELL_DATA_UPDATED} from '@/store/actions/cell.js';
-import tmActions from '@/store/actions/threatmodel.js';
-import dataChanged from '@/service/x6/graph/data-changed.js';
-import threatModels from '@/service/threats/models/index.js';
-export default {
-    name: 'TdThreatSuggest',
-    computed: {
-        ...mapState({
-            cellRef: (state) => state.cell.ref,
-            modelType: (state) => state.threatmodel.selectedDiagram.diagramType,
-            threatTop: (state) => state.threatmodel.data.detail.threatTop
-        }),
-        threatTypes(){
-            if(!this.cellRef || !this.threat || !this.modelType)
-                return [];
-            const res = [];
-            const threattypes = threatModels.getThreatTypesByElement(this.modelType, this.cellRef.data.type);
-            Object.keys(threattypes).forEach((type)=>{
-                res.push(this.$t(type));
-            }, this);
-            return res;
-        },
-        statuses() {
-            return [
-                { value: 'NotApplicable', text: this.$t('threats.status.notApplicable') },
-                { value: 'Open', text: this.$t('threats.status.open') },
-                { value: 'Mitigated', text: this.$t('threats.status.mitigated') }
-            ];
-        },
-        priorities() {
-            return [
-                { value: 'Low', text: this.$t('threats.priority.low') },
-                { value: 'Medium', text: this.$t('threats.priority.medium') },
-                { value: 'High', text: this.$t('threats.priority.high') }
-            ];
-        },
-        modalTitle() { return this.$t('threats.newThreatByType') + ' #' + (this.threatTop+1); }
-    },
-    data() {
-        return {
-            threat: {},
-            index:0
-        };
-    },
-    methods: {
-        showModal() {
-            this.modalTitle;
-            this.threat = createNewTypedThreat(this.modelType,this.cellRef.data.type,this.threatTop+1);
-            this.$refs.editModal.show();
-        },
-        hideModal(){
-            this.threat={};
-            this.index=0;
-            this.$refs.editModal.hide();
-        },
-        next(){
-            this.index++;
-            if(this.index>=this.threatTypes.length){
-                this.$refs.editModal.hide();
-                this.index=0;
-                return;
-            }
-            this.threat.type = this.threatTypes[this.index];
-        },
-        acceptSuggestion(){
-            const objRef = this.cellRef.data;
-            if(!objRef.threatFrequency){
-                const tmpfreq = threatModels.getFrequencyMapByElement(this.threat.modelType,this.cellRef.data.type);
-                if(tmpfreq!==null)
-                    objRef.threatFrequency = tmpfreq;
-            }
-            if(objRef.threatFrequency){
-                Object.keys(objRef.threatFrequency).forEach((k)=>{
-                    if(this.$t(`threats.model.${this.modelType.toLowerCase()}.${k}`)===this.threat.type)
-                        objRef.threatFrequency[k]++;
-                });
-            }
-            this.threat.new=false;
-            this.cellRef.data.threats.push(this.threat);
-            this.cellRef.data.hasOpenThreats = this.cellRef.data.threats.length > 0;
-            this.$store.dispatch(tmActions.update, {threatTop : this.threatTop+1});
-            this.$store.dispatch(tmActions.modified);
-            this.$store.dispatch(CELL_DATA_UPDATED,this.cellRef.data);
-            dataChanged.updateStyleAttrs(this.cellRef);
-            this.next();
-            
-        },
-        previous(){
-            if(this.index>0){
-                this.index--;
-                this.threat.type = this.threatTypes[this.index];
-            }
-        }
-    }
-};
-</script>
\ No newline at end of file
diff --git a/td.vue/src/desktop/menu.js b/td.vue/src/desktop/menu.js
index b71ae5dd..8c24e130 100644
--- a/td.vue/src/desktop/menu.js
+++ b/td.vue/src/desktop/menu.js
@@ -42,8 +42,8 @@ export const model = {
 };
 
 export function getMenuTemplate () {
-    return [
-        ...(isMacOS ? [{ role: 'appMenu' }] : []),
+    var menuTemplate = (isMacOS ? [{ role: 'appMenu' }] : []);
+    menuTemplate.push(
         {
             label: messages[language].desktop.file.heading,
             submenu: [
@@ -53,16 +53,6 @@ export function getMenuTemplate () {
                         openModelRequest('');
                     }
                 },
-                {
-                    label: messages[language].desktop.file.recentDocs,
-                    role: 'recentdocuments',
-                    submenu: [
-                        {
-                            label: messages[language].desktop.file.clearRecentDocs,
-                            role: 'clearrecentdocuments'
-                        }
-                    ]
-                },
                 {
                     label: messages[language].desktop.file.save,
                     click () {
@@ -166,7 +156,25 @@ export function getMenuTemplate () {
                 { role: 'about' }
             ]
         }
-    ];
+    );
+
+    if (isMacOS) {
+        // recent docs only for macos, see www.electronjs.org/docs/latest/api/menu-item#roles
+        menuTemplate[1].submenu.push(
+            {
+                label: messages[language].desktop.file.recentDocs,
+                role: 'recentdocuments',
+                submenu: [
+                    {
+                        label: messages[language].desktop.file.clearRecentDocs,
+                        role: 'clearrecentdocuments'
+                    }
+                ]
+            }
+        );
+    }
+
+    return menuTemplate;
 }
 
 // Open file system dialog and read file contents into model
@@ -344,7 +352,8 @@ function savePDFReport (pdfPath) {
     var dialogOptions = {
         title: messages[language].forms.exportPdf,
         defaultPath: pdfPath,
-        filters: [{ name: 'PDF report', extensions: ['.pdf'] }, { name: 'All Files', extensions: ['*'] }]
+        properties: ['openFile'],
+        filters: [{ name: 'PDF report', extensions: ['pdf'] }, { name: 'All Files', extensions: ['*'] }]
     };
 
     dialog.showSaveDialog(dialogOptions).then(result => {
diff --git a/td.vue/src/i18n/en.js b/td.vue/src/i18n/en.js
index daea511b..ac35da54 100644
--- a/td.vue/src/i18n/en.js
+++ b/td.vue/src/i18n/en.js
@@ -253,9 +253,7 @@ const eng = {
         saveAs: 'Save As',
         saveModel: 'Save Model',
         saveModelAs: 'Save Model As',
-        search: 'Search',
-        next:'next',
-        previous:'previous'
+        search: 'Search'
     },
     threats: {
         model: {
diff --git a/td.vue/src/service/api/threatmodelApi.js b/td.vue/src/service/api/threatmodelApi.js
index 6b2b0d70..e45664fc 100644
--- a/td.vue/src/service/api/threatmodelApi.js
+++ b/td.vue/src/service/api/threatmodelApi.js
@@ -8,6 +8,10 @@ const extractRepoParts = (fullRepoName) => {
     return { org, repo };
 };
 
+const encodeUrlComponents = (... uriComponents) => {
+    return uriComponents.map(uriComponent => encodeURIComponent(uriComponent));
+};
+
 
 /**
  * Gets the organisation data configured for the express server
@@ -30,7 +34,8 @@ const reposAsync = (page = 1) => {
  */
 const branchesAsync = (fullRepoName, page = 1) => {
     const { org, repo } = extractRepoParts(fullRepoName);
-    return api.getAsync(`${resource}/${org}/${repo}/branches`, { params: { page: page } });
+    const [ encodedOrg, encodedRepo ] = encodeUrlComponents(org, repo);
+    return api.getAsync(`${resource}/${encodedOrg}/${encodedRepo}/branches`, { params: { page: page } });
 };
 
 /**
@@ -41,7 +46,8 @@ const branchesAsync = (fullRepoName, page = 1) => {
  */
 const modelsAsync = (fullRepoName, branch) => {
     const { org, repo } = extractRepoParts(fullRepoName);
-    return api.getAsync(`${resource}/${org}/${repo}/${encodeURIComponent(branch)}/models`);
+    const [ encodedOrg, encodedRepo, encodedBranch ] = encodeUrlComponents(org, repo, branch);
+    return api.getAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/models`);
 };
 
 /**
@@ -53,17 +59,20 @@ const modelsAsync = (fullRepoName, branch) => {
  */
 const modelAsync = (fullRepoName, branch, model) => {
     const { org, repo } = extractRepoParts(fullRepoName);
-    return api.getAsync(`${resource}/${org}/${repo}/${branch}/${model}/data`);
+    const [ encodedOrg, encodedRepo, encodedBranch, encodedModel ] = encodeUrlComponents(org, repo, branch, model);
+    return api.getAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModel}/data`);
 };
 
 const createAsync = (fullRepoName, branch, modelName, threatModel) => {
     const { org, repo } = extractRepoParts(fullRepoName);
-    return api.postAsync(`${resource}/${org}/${repo}/${branch}/${modelName}/create`, threatModel);
+    const [ encodedOrg, encodedRepo, encodedBranch, encodedModelName ] = encodeUrlComponents(org, repo, branch, modelName);
+    return api.postAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModelName}/create`, threatModel);
 };
 
 const updateAsync = (fullRepoName, branch, modelName, threatModel) => {
     const { org, repo } = extractRepoParts(fullRepoName);
-    return api.putAsync(`${resource}/${org}/${repo}/${branch}/${modelName}/update`, threatModel);
+    const [ encodedOrg, encodedRepo, encodedBranch, encodedModelName ] = encodeUrlComponents(org, repo, branch, modelName);
+    return api.putAsync(`${resource}/${encodedOrg}/${encodedRepo}/${encodedBranch}/${encodedModelName}/update`, threatModel);
 };
 
 export default {
diff --git a/td.vue/src/views/ThreatModelEdit.vue b/td.vue/src/views/ThreatModelEdit.vue
index d7f454b4..de47b7a8 100644
--- a/td.vue/src/views/ThreatModelEdit.vue
+++ b/td.vue/src/views/ThreatModelEdit.vue
@@ -82,6 +82,8 @@
                                     v-model="contributors"
                                     @input="onModifyModel()"
                                     variant="primary"
+                                    separator=",;"
+                                    tag-class="mx-2"
                                 ></b-form-tags>
                             </b-form-group>
                         </b-col>


Test patch:
diff --git a/td.server/test/controllers/healthcheck.spec.js b/td.server/test/controllers/healthz.spec.js
similarity index 100%
rename from td.server/test/controllers/healthcheck.spec.js
rename to td.server/test/controllers/healthz.spec.js
diff --git a/td.server/test/env/Bitbucket.spec.js b/td.server/test/env/Bitbucket.spec.js
index 474a5e71..5fd938d5 100644
--- a/td.server/test/env/Bitbucket.spec.js
+++ b/td.server/test/env/Bitbucket.spec.js
@@ -1,7 +1,7 @@
-import Bitbucket from '../../src/env/Bitbucket.js';
-
 import { expect } from 'chai';
+
 import { Env } from '../../src/env/Env.js';
+import Bitbucket from '../../src/env/Bitbucket.js';
 
 describe('env/Bitbucket.js', () => {
     let bitbucketEnv;
@@ -43,10 +43,59 @@ describe('env/Bitbucket.js', () => {
         expect(isRequired).to.be.false;
     });
 
+    it('has a default value for property SCOPE', () => {
+        const value = bitbucketEnv.properties
+            .find(x => x.key === 'SCOPE')
+            .defaultValue;
+        expect(value).to.equal('repository:read');
+    });
+
+    it('has the optional property ENTERPRISE_HOSTNAME', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'ENTERPRISE_HOSTNAME')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property ENTERPRISE_PORT', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PORT')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property ENTERPRISE_PORT', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PORT')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property ENTERPRISE_PROTOCOL', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PROTOCOL')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property ENTERPRISE_PROTOCOL', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PROTOCOL')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
     it('has the optional property WORKSPACE', () => {
         const isRequired = bitbucketEnv.properties
             .find(x => x.key === 'WORKSPACE')
             .required;
         expect(isRequired).to.be.false;
     });
+
+    it('has the optional property REPO_ROOT_DIRECTORY', () => {
+        const isRequired = bitbucketEnv.properties
+            .find(x => x.key === 'REPO_ROOT_DIRECTORY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
 });
diff --git a/td.server/test/env/Github.spec.js b/td.server/test/env/Github.spec.js
index ef9d6adc..bbb0115e 100644
--- a/td.server/test/env/Github.spec.js
+++ b/td.server/test/env/Github.spec.js
@@ -42,4 +42,74 @@ describe('env/Github.js', () => {
             .required;
         expect(isRequired).to.be.false;
     });
+
+    it('has a default value for property SCOPE', () => {
+        const value = githubEnv.properties
+            .find(x => x.key === 'SCOPE')
+            .defaultValue;
+        expect(value).to.equal('public_repo');
+    });
+
+    it('has the optional property ENTERPRISE_HOSTNAME', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'ENTERPRISE_HOSTNAME')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property ENTERPRISE_PORT', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PORT')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property ENTERPRISE_PORT', () => {
+        const value = githubEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PORT')
+            .defaultValue;
+        expect(value).to.equal(443);
+    });
+
+    it('has the optional property ENTERPRISE_PROTOCOL', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PROTOCOL')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property ENTERPRISE_PROTOCOL', () => {
+        const value = githubEnv.properties
+            .find(x => x.key === 'ENTERPRISE_PROTOCOL')
+            .defaultValue;
+        expect(value).to.equal('https');
+    });
+
+    it('has the optional property USE_SEARCH', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'USE_SEARCH')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property USE_SEARCH', () => {
+        const value = githubEnv.properties
+            .find(x => x.key === 'USE_SEARCH')
+            .defaultValue;
+        expect(value).to.be.false;
+    });
+
+    it('has the optional property SEARCH_QUERY', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'SEARCH_QUERY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property REPO_ROOT_DIRECTORY', () => {
+        const isRequired = githubEnv.properties
+            .find(x => x.key === 'REPO_ROOT_DIRECTORY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
 });
diff --git a/td.server/test/env/Gitlab.spec.js b/td.server/test/env/Gitlab.spec.js
new file mode 100644
index 00000000..c06e1894
--- /dev/null
+++ b/td.server/test/env/Gitlab.spec.js
@@ -0,0 +1,73 @@
+import { expect } from 'chai';
+
+import { Env } from '../../src/env/Env.js';
+import Gitlab from '../../src/env/Gitlab.js';
+
+describe('env/Gitlab.js', () => {
+    let gitlabEnv;
+
+    beforeEach(() => {
+        gitlabEnv = new Gitlab();
+    });
+
+    it('extends Env', () => {
+        expect(gitlabEnv).is.instanceOf(Env);
+    });
+
+    it('is named bitbucket', () => {
+        expect(gitlabEnv.name).to.eq('Gitlab');
+    });
+
+    it('uses the GITLAB_ prefix', () => {
+        expect(gitlabEnv.prefix).to.eq('GITLAB_');
+    });
+
+    it('has the optional property CLIENT_ID', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'CLIENT_ID')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property CLIENT_SECRET', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'CLIENT_SECRET')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property SCOPE', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'SCOPE')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property SCOPE', () => {
+        const value = gitlabEnv.properties
+            .find(x => x.key === 'SCOPE')
+            .defaultValue;
+        expect(value).to.equal('read_user read_repository');
+    });
+
+    it('has the optional property HOST', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'HOST')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property REDIRECT_URI', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'REDIRECT_URI')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has the optional property REPO_ROOT_DIRECTORY', () => {
+        const isRequired = gitlabEnv.properties
+            .find(x => x.key === 'REPO_ROOT_DIRECTORY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+});
diff --git a/td.server/test/env/ThreatDragon.spec.js b/td.server/test/env/ThreatDragon.spec.js
index 4d80e6cb..1247545d 100644
--- a/td.server/test/env/ThreatDragon.spec.js
+++ b/td.server/test/env/ThreatDragon.spec.js
@@ -29,6 +29,13 @@ describe('env/ThreatDragon.js', () => {
         expect(isRequired).to.be.false;
     });
 
+    it('has a default value for property NODE_ENV', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'NODE_ENV')
+            .defaultValue;
+        expect(value).to.equal('production');
+    });
+
     it('has the optional property PORT', () => {
         const isRequired = tdEnv.properties
             .find(x => x.key === 'PORT')
@@ -36,6 +43,13 @@ describe('env/ThreatDragon.js', () => {
         expect(isRequired).to.be.false;
     });
 
+    it('has a default value for property PORT', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'PORT')
+            .defaultValue;
+        expect(value).to.equal(3000);
+    });
+
     it('has the optional property LOG_MAX_FILE_SIZE', () => {
         const isRequired = tdEnv.properties
             .find(x => x.key === 'LOG_MAX_FILE_SIZE')
@@ -43,6 +57,13 @@ describe('env/ThreatDragon.js', () => {
         expect(isRequired).to.be.false;
     });
 
+    it('has a default value for property LOG_MAX_FILE_SIZE', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'LOG_MAX_FILE_SIZE')
+            .defaultValue;
+        expect(value).to.equal(24);
+    });
+
     it('has the required property LOG_LEVEL', () => {
         const isRequired = tdEnv.properties
             .find(x => x.key === 'LOG_LEVEL')
@@ -50,10 +71,59 @@ describe('env/ThreatDragon.js', () => {
         expect(isRequired).to.be.false;
     });
 
+    it('has a default value for property LOG_LEVEL', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'LOG_LEVEL')
+            .defaultValue;
+        expect(value).to.equal('warn');
+    });
+
     it('has the optional property SERVER_API_PROTOCOL', () => {
         const isRequired = tdEnv.properties
             .find(x => x.key === 'SERVER_API_PROTOCOL')
             .required;
         expect(isRequired).to.be.false;
     });
+
+    it('has a default value for property SERVER_API_PROTOCOL', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'SERVER_API_PROTOCOL')
+            .defaultValue;
+        expect(value).to.equal('https');
+    });
+
+    it('has the optional property REPO_ROOT_DIRECTORY', () => {
+        const isRequired = tdEnv.properties
+            .find(x => x.key === 'REPO_ROOT_DIRECTORY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property REPO_ROOT_DIRECTORY', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'REPO_ROOT_DIRECTORY')
+            .defaultValue;
+        expect(value).to.equal('ThreatDragonModels');
+    });
+
+    it('has the optional property REPO_USE_SEARCH', () => {
+        const isRequired = tdEnv.properties
+            .find(x => x.key === 'REPO_USE_SEARCH')
+            .required;
+        expect(isRequired).to.be.false;
+    });
+
+    it('has a default value for property REPO_USE_SEARCH', () => {
+        const value = tdEnv.properties
+            .find(x => x.key === 'REPO_USE_SEARCH')
+            .defaultValue;
+        expect(value).to.be.false;
+    });
+
+    it('has the optional property REPO_SEARCH_QUERY', () => {
+        const isRequired = tdEnv.properties
+            .find(x => x.key === 'REPO_SEARCH_QUERY')
+            .required;
+        expect(isRequired).to.be.false;
+    });
 });
diff --git a/td.vue/tests/unit/desktop/menu.spec.js b/td.vue/tests/unit/desktop/menu.spec.js
index ecbb8d40..3362a6b5 100644
--- a/td.vue/tests/unit/desktop/menu.spec.js
+++ b/td.vue/tests/unit/desktop/menu.spec.js
@@ -56,15 +56,6 @@ describe('desktop/menu.js', () => {
             expect(openModelItem).toBeDefined();
         });
 
-        it('contains recent items', () => {
-            const recentItems = {
-                'label': 'Open Recent',
-                'role': 'recentdocuments',
-                'submenu': [{'label': 'Clear Menu', 'role': 'clearrecentdocuments'}]
-            };
-            expect(fileItems.submenu).toContainEqual(recentItems);
-        });
-
         it('contains save model', () => {
             const saveModelItem = fileItems.submenu.find((item) => item.label === 'Save Model');
             expect(saveModelItem).toBeDefined();
diff --git a/td.vue/tests/unit/service/api/threatmodelApi.spec.js b/td.vue/tests/unit/service/api/threatmodelApi.spec.js
index 0dbc7ea2..c9e66447 100644
--- a/td.vue/tests/unit/service/api/threatmodelApi.spec.js
+++ b/td.vue/tests/unit/service/api/threatmodelApi.spec.js
@@ -28,19 +28,19 @@ describe('service/threatmodelApi.js', () => {
     });
 
     describe('branchesAsync', () => {
-        const repo = 'owasp/threat-dragon';
+        const repo = 'owasp/with/deep/layers/threat-dragon';
 
         beforeEach(async () => {
             await threatmodelApi.branchesAsync(repo);
         });
 
         it('calls the branches endpoint', () => {
-            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/threat-dragon/branches', {'params': {'page': 1}});
+            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/with%2Fdeep%2Flayers%2Fthreat-dragon/branches', {'params': {'page': 1}});
         });
     });
 
     describe('modelsAsync', () => {
-        const repo = 'owasp/threat-dragon';
+        const repo = 'owasp/with/deep/layers/threat-dragon';
         const branch = 'feature/some-feature';
 
         beforeEach(async () => {
@@ -48,28 +48,28 @@ describe('service/threatmodelApi.js', () => {
         });
 
         it('calls the models endpoint', () => {
-            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/threat-dragon/feature%2Fsome-feature/models');
+            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/with%2Fdeep%2Flayers%2Fthreat-dragon/feature%2Fsome-feature/models');
         });
     });
 
     describe('modelAsync', () => {
-        const repo = 'owasp/threat-dragon';
-        const branch = 'main';
-        const model = 'test';
+        const repo = 'owasp/with/deep/layers/threat-dragon';
+        const branch = 'main&';
+        const model = 'test?';
 
         beforeEach(async () => {
             await threatmodelApi.modelAsync(repo, branch, model);
         });
 
         it('calls the model endpoint', () => {
-            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/threat-dragon/main/test/data');
+            expect(api.getAsync).toHaveBeenCalledWith('/api/threatmodel/owasp/with%2Fdeep%2Flayers%2Fthreat-dragon/main%26/test%3F/data');
         });
     });
 
     describe('updateAsync', () => {
-        const repo = 'owasp/threat-dragon';
-        const branch = 'main';
-        const modelName = 'test';
+        const repo = 'owasp/with/deep/layers/threat-dragon';
+        const branch = 'main&';
+        const modelName = 'test?';
         const body = { foo: 'bar' };
 
         beforeEach(async () => {
@@ -78,7 +78,7 @@ describe('service/threatmodelApi.js', () => {
 
         it('calls the update endpoint', () => {
             expect(api.putAsync).toHaveBeenCalledWith(
-                '/api/threatmodel/owasp/threat-dragon/main/test/update',
+                '/api/threatmodel/owasp/with%2Fdeep%2Flayers%2Fthreat-dragon/main%26/test%3F/update',
                 body
             );
         });