From 29647ebd05c78272ae0c97a754cd0b572e275b48 Mon Sep 17 00:00:00 2001 From: Caroline BANCHEREAU Date: Tue, 15 Jul 2025 11:10:53 +0200 Subject: [PATCH 1/2] aligned frontend --- frontend/package-lock.json | 135 ++++--- frontend/package.json | 9 +- frontend/src/app/app.component.ts | 6 +- frontend/src/app/app.module.ts | 43 --- .../src/app/chatzone/chatzone.component.ts | 24 +- .../src/app/heatmap/heatmap.component.html | 31 -- frontend/src/app/heatmap/heatmap.component.ts | 349 ++++++++---------- frontend/src/app/types/API.ts | 51 ++- frontend/src/app/utils/utils.ts | 13 +- frontend/src/main.ts | 27 +- 10 files changed, 306 insertions(+), 382 deletions(-) delete mode 100755 frontend/src/app/app.module.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 505a70b..e8630b3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,10 +29,10 @@ "devDependencies": { "@angular-devkit/build-angular": "^19.2.0", "@angular-eslint/builder": "^19.1.0", - "@angular-eslint/eslint-plugin": "^19.1.0", - "@angular-eslint/eslint-plugin-template": "^19.1.0", + "@angular-eslint/eslint-plugin": "^19.4.0", + "@angular-eslint/eslint-plugin-template": "^19.4.0", "@angular-eslint/schematics": "^19.1.0", - "@angular-eslint/template-parser": "^19.1.0", + "@angular-eslint/template-parser": "^19.4.0", "@angular/cli": "^19.2.0", "@angular/compiler-cli": "^19.2.0", "@eslint/js": "^9.27.0", @@ -362,21 +362,19 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.1.0.tgz", - "integrity": "sha512-HUJyukRvnh8Z9lIdxdblBRuBaPYEVv4iAYZMw3d+dn4rrM27Nt5oh3/zkwYrrPkt36tZdeXdDWrOuz9jgjVN5w==", - "dev": true, - "license": "MIT" + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.4.0.tgz", + "integrity": "sha512-Djq+je34czagDxvkBbbe1dLlhUGYK2MbHjEgPTQ00tVkacLQGAW4UmT1A0JGZzfzl/lDVvli64/lYQsJTSSM6A==", + "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.1.0.tgz", - "integrity": "sha512-TDO0+Ry+oNkxnaLHogKp1k2aey6IkJef5d7hathE4UFT6owjRizltWaRoX6bGw7Qu1yagVLL8L2Se8SddxSPAQ==", + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.4.0.tgz", + "integrity": "sha512-jXhyYYIdo5ItCFfmw7W5EqDRQx8rYtiYbpezI84CemKPHB/VPiP/zqLIvdTVBdJdXlqS31ueXn2YlWU0w6AAgg==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.1.0", - "@angular-eslint/utils": "19.1.0" + "@angular-eslint/bundled-angular-compiler": "19.4.0", + "@angular-eslint/utils": "19.4.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -385,14 +383,13 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.1.0.tgz", - "integrity": "sha512-bIUizkCY40mnU8oAO1tLV7uN2H/cHf1evLlhpqlb9JYwc5dT2moiEhNDo61OtOgkJmDGNuThAeO9Xk9hGQc7nA==", + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.4.0.tgz", + "integrity": "sha512-6WAGnHf5SKi7k8/AOOLwGCoN3iQUE8caKsg0OucL4CWPUyzsYpQjx7ALKyxx9lqoAngn3CTlQ2tcwDv6aYtfmg==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "19.1.0", - "@angular-eslint/utils": "19.1.0", + "@angular-eslint/bundled-angular-compiler": "19.4.0", + "@angular-eslint/utils": "19.4.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, @@ -419,27 +416,50 @@ "strip-json-comments": "3.1.1" } }, - "node_modules/@angular-eslint/template-parser": { + "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.1.0.tgz", + "integrity": "sha512-HUJyukRvnh8Z9lIdxdblBRuBaPYEVv4iAYZMw3d+dn4rrM27Nt5oh3/zkwYrrPkt36tZdeXdDWrOuz9jgjVN5w==", + "dev": true + }, + "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/eslint-plugin": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.1.0.tgz", - "integrity": "sha512-wbMi7adlC+uYqZo7NHNBShpNhFJRZsXLqihqvFpAUt1Ei6uDX8HR6MyMEDZ9tUnlqtPVW5nmbedPyLVG7HkjAA==", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.1.0.tgz", + "integrity": "sha512-TDO0+Ry+oNkxnaLHogKp1k2aey6IkJef5d7hathE4UFT6owjRizltWaRoX6bGw7Qu1yagVLL8L2Se8SddxSPAQ==", "dev": true, - "license": "MIT", "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.1.0", - "eslint-scope": "^8.0.2" + "@angular-eslint/utils": "19.1.0" }, "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, - "node_modules/@angular-eslint/utils": { + "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/eslint-plugin-template": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.1.0.tgz", + "integrity": "sha512-bIUizkCY40mnU8oAO1tLV7uN2H/cHf1evLlhpqlb9JYwc5dT2moiEhNDo61OtOgkJmDGNuThAeO9Xk9hGQc7nA==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.1.0", + "@angular-eslint/utils": "19.1.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/@angular-eslint/utils": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.1.0.tgz", "integrity": "sha512-mcb7hPMH/u6wwUwvsewrmgb9y9NWN6ZacvpUvKlTOxF/jOtTdsu0XfV4YB43sp2A8NWzYzX0Str4c8K1xSmuBQ==", "dev": true, - "license": "MIT", "dependencies": { "@angular-eslint/bundled-angular-compiler": "19.1.0" }, @@ -449,6 +469,34 @@ "typescript": "*" } }, + "node_modules/@angular-eslint/template-parser": { + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.4.0.tgz", + "integrity": "sha512-f4t7Z6zo8owOTUqAtZ3G/cMA5hfT3RE2OKR0dLn7YI6LxUJkrlcHq75n60UHiapl5sais6heo70hvjQgJ3fDxQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.4.0", + "eslint-scope": "^8.0.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "19.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.4.0.tgz", + "integrity": "sha512-2hZ7rf/0YBkn1Rk0i7AlYGlfxQ7+DqEXUsgp1M56mf0cy7/GCFiWZE0lcXFY4kzb4yQK3G2g+kIF092MwelT7Q==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.4.0" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, "node_modules/@angular/animations": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.0.tgz", @@ -3491,7 +3539,6 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, - "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -3520,7 +3567,6 @@ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", @@ -3535,7 +3581,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3546,7 +3591,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3569,7 +3613,6 @@ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -3582,7 +3625,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -3606,7 +3648,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3623,7 +3664,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3634,7 +3674,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" }, @@ -3647,7 +3686,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -3656,15 +3694,13 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3677,7 +3713,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -3690,7 +3725,6 @@ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -3700,7 +3734,6 @@ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" @@ -7312,7 +7345,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", @@ -7342,7 +7374,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", @@ -7367,7 +7398,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" @@ -7385,7 +7415,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", @@ -7409,7 +7438,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7423,7 +7451,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", @@ -7450,7 +7477,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", @@ -7474,7 +7500,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, - "license": "MIT", "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" @@ -7492,7 +7517,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -7752,7 +7776,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -10592,7 +10615,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -10653,7 +10675,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -10754,7 +10775,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", @@ -10772,7 +10792,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -19033,7 +19052,6 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18.12" }, @@ -19412,7 +19430,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/package.json b/frontend/package.json index b97b06f..2a4ea62 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,13 +1,14 @@ { "name": "stars", "version": "0.0.2", + "type": "module", "scripts": { "ng": "ng", "start": "cp src/assets/configs/config.local.json src/assets/configs/config.json && ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "lint": "ng lint" + "lint": "eslint . --ext .ts,.html" }, "private": true, "dependencies": { @@ -32,10 +33,10 @@ "devDependencies": { "@angular-devkit/build-angular": "^19.2.0", "@angular-eslint/builder": "^19.1.0", - "@angular-eslint/eslint-plugin": "^19.1.0", - "@angular-eslint/eslint-plugin-template": "^19.1.0", + "@angular-eslint/eslint-plugin": "^19.4.0", + "@angular-eslint/eslint-plugin-template": "^19.4.0", "@angular-eslint/schematics": "^19.1.0", - "@angular-eslint/template-parser": "^19.1.0", + "@angular-eslint/template-parser": "^19.4.0", "@angular/cli": "^19.2.0", "@angular/compiler-cli": "^19.2.0", "@eslint/js": "^9.27.0", diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7c54a23..bbf3155 100755 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,10 +1,12 @@ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], - standalone: false + imports: [RouterOutlet], + standalone: true, }) export class AppComponent { title = 'application'; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts deleted file mode 100755 index 3eccf9a..0000000 --- a/frontend/src/app/app.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { BrowserModule } from '@angular/platform-browser'; -import { ChatzoneComponent } from './chatzone/chatzone.component'; -import { FormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; -import { MaterialModule } from './material.module'; -import { NgModule } from '@angular/core'; -import { MarkdownModule } from 'ngx-markdown'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { APP_INITIALIZER } from '@angular/core'; -import { ConfigService } from './services/config.service'; -import { lastValueFrom } from 'rxjs'; - -export function initializeApp(configService: ConfigService) { - return () => lastValueFrom(configService.loadConfig()); -} - -@NgModule({ - declarations: [ - AppComponent, - ChatzoneComponent - ], - imports: [ - BrowserModule, - AppRoutingModule, - FormsModule, - HttpClientModule, - BrowserAnimationsModule, - MaterialModule, - MarkdownModule.forRoot(), - MatProgressBarModule, - ], - providers: [{ - provide: APP_INITIALIZER, - useFactory: initializeApp, - deps: [ConfigService], - multi: true - }], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/frontend/src/app/chatzone/chatzone.component.ts b/frontend/src/app/chatzone/chatzone.component.ts index bf24e35..162113b 100644 --- a/frontend/src/app/chatzone/chatzone.component.ts +++ b/frontend/src/app/chatzone/chatzone.component.ts @@ -1,16 +1,22 @@ -import {APIResponse, ReportItem} from '../types/API'; -import {AfterViewChecked, AfterViewInit, Component, ElementRef, QueryList, ViewChildren} from '@angular/core'; -import {ChatItem, Message, ReportCard, VulnerabilityReportCard} from '../types/ChatItem'; -import {Status, Step} from '../types/Step'; - -import {VulnerabilityInfoService} from '../services/vulnerability-information.service'; -import {WebSocketService} from '../services/web-socket.service'; +import { APIResponse, ReportItem } from '../types/API'; +import { AfterViewChecked, AfterViewInit, Component, ElementRef, QueryList, ViewChildren } from '@angular/core'; +import { ChatItem, Message, ReportCard, VulnerabilityReportCard } from '../types/ChatItem'; +import { Status, Step } from '../types/Step'; + +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MarkdownModule } from 'ngx-markdown'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MaterialModule } from '../material.module'; +import { VulnerabilityInfoService } from '../services/vulnerability-information.service'; +import { WebSocketService } from '../services/web-socket.service'; @Component({ selector: 'app-chatzone', templateUrl: './chatzone.component.html', styleUrls: ['./chatzone.component.css'], - standalone: false, + imports: [MaterialModule, MarkdownModule, MatProgressBarModule, FormsModule, CommonModule], + standalone: true, }) export class ChatzoneComponent implements AfterViewInit, AfterViewChecked { chatItems: ChatItem[]; @@ -30,11 +36,9 @@ export class ChatzoneComponent implements AfterViewInit, AfterViewChecked { this.ws.webSocket$.subscribe({ next: (value: any) => { - // eslint-disable-line @typescript-eslint/no-explicit-any this.handleWSMessage(value as APIResponse); }, error: (error: any) => { - // eslint-disable-line @typescript-eslint/no-explicit-any console.log(error); if (error?.type != 'close') { // Close is already handled via the isConnected call diff --git a/frontend/src/app/heatmap/heatmap.component.html b/frontend/src/app/heatmap/heatmap.component.html index eda4d0f..73f8a45 100644 --- a/frontend/src/app/heatmap/heatmap.component.html +++ b/frontend/src/app/heatmap/heatmap.component.html @@ -1,36 +1,5 @@ - - - -
- STARS Results Heatmap
- - -
-
- - - -
-
- -
- - Select a Vendor - - - All vendors - {{ vendor }} - - - overview -
- -
- diff --git a/frontend/src/app/heatmap/heatmap.component.ts b/frontend/src/app/heatmap/heatmap.component.ts index ee36f7d..9994055 100644 --- a/frontend/src/app/heatmap/heatmap.component.ts +++ b/frontend/src/app/heatmap/heatmap.component.ts @@ -1,16 +1,16 @@ -import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit} from '@angular/core'; -import {Observable, map} from 'rxjs'; -import {capitalizeFirstLetter, generateModelName, splitModelName} from '../utils/utils'; +import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnInit } from '@angular/core'; +import { capitalizeFirstLetter, splitModelName } from '../utils/utils'; import ApexCharts from 'apexcharts'; -import {CommonModule} from '@angular/common'; -import {FormsModule} from '@angular/forms'; -import {HttpClient} from '@angular/common/http'; -import {MatButtonModule} from '@angular/material/button'; -import {MatCardModule} from '@angular/material/card'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {MatSelectModule} from '@angular/material/select'; -import {environment} from '../../environments/environment'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { HttpClient } from '@angular/common/http'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { ScoreResponse } from './../types/API'; +import { environment } from '../../environments/environment'; @Component({ selector: 'app-heatmap', @@ -20,172 +20,113 @@ import {environment} from '../../environments/environment'; imports: [CommonModule, MatFormFieldModule, MatSelectModule, FormsModule, MatCardModule, MatButtonModule], }) export class HeatmapComponent implements AfterViewInit, OnInit { - public heatmapData: number[][] = []; - // for UI dropdown menu of vendors - public vendorsNames: string[] = []; - public selectedVendor: string = ''; - public weightedAttacks: {attackName: string; weight: string}[] = []; - constructor(private http: HttpClient, private el: ElementRef, private changeDetector: ChangeDetectorRef) {} ngAfterViewInit() { - this.createHeatmap([]); // Initialisation avec des données vides + this.createHeatmap({ + attacks: [], + models: [], + }); // Initialize empty heatmap to avoid errors before data is loaded } ngOnInit() { - // this.loadHeatmapData('amazon'); - this.loadVendorsData(); - this.loadHeatmapData(''); - } - - onFileSelected(event: any) { - // const file = event.target.files[0]; - // if (!file) return; - // const formData = new FormData(); - // formData.append('file', file); - // this.http.post('http://localhost:3000/upload', formData).subscribe({ - // next: data => { - // console.log('📊 Données reçues via upload:', data); - // this.processData(data); - // }, - // error: error => console.error('❌ Erreur upload:', error), - // }); - } - - //load a dropdown menu from the loadModelsData result - loadVendorsData() { - // this.http.get(`http://127.0.0.1:8080/api/vendors`).subscribe({ - this.http.get(`${environment.api_url}/api/vendors`).subscribe({ - next: data => { - console.log('📡 Données brutes reçues du serveur:', data); - this.processVendors(data.map(vendor => vendor)); - }, - error: error => console.error('❌ Erreur API:', error), - }); + this.loadHeatmapData(); } - //load the heatmap data from the server with a name in params - loadHeatmapData(vendor: string) { + // Load the heatmap data from the server + loadHeatmapData() { let url = ''; - if (!vendor) { - url = `${environment.api_url}/api/heatmap`; - } else { - url = `${environment.api_url}/api/heatmap/${vendor}`; - } - this.http.get(url).subscribe({ - // this.http.get(`${environment.api_url}/api/${vendor}`).subscribe({ + url = `${environment.api_url}/api/heatmap`; + this.http.get(url).subscribe({ next: scoresData => { - this.processData(scoresData, vendor); + this.processDataAfterScan(scoresData); }, error: error => console.error('❌ Erreur API:', error), }); } - // handle models name recieved from the server to a list used in frontend for a dropdown menu - processVendors(vendorsNames: string[]) { - this.vendorsNames = vendorsNames.map(capitalizeFirstLetter); + // Construct the heatmap data from the API response + processDataAfterScan(data: ScoreResponse) { + let modelNames: string[] = []; + let attackNames: string[] = []; + modelNames = data.models.map(model => model.name); + attackNames = data.attacks.map(attack => attack.name); + this.createHeatmap(data, modelNames, attackNames); } - processData(data: any[], vendor: string = '') { - const modelNames = generateModelName(data, vendor); - this.getWeightedAttacks().subscribe({ - next: weightedAttacks => { - this.heatmapData = data.map(row => { - const rowData = weightedAttacks.map(attack => { - const value = Number(row[attack.attackName]?.trim()); - return isNaN(value) ? 0 : value * 10; - }); - let totalWeights = 0; - // Add an extra column at the end with a custom calculation (modify as needed) - const weightedSumColumn = weightedAttacks.reduce((sum, {attackName, weight}) => { - const value = Number(row[attackName]?.trim()); - const weightedValue = isNaN(value) ? 0 : value * Number(weight); - totalWeights = totalWeights + Number(weight); - return sum + weightedValue; - }, 0); - // Append the calculated weighted sum column to the row as the last column "as an attack" even if it's a custom calculated value - return [...rowData, (weightedSumColumn / totalWeights) * 10]; - }); - const attackNames = weightedAttacks.map(attack => attack.attackName); - this.createHeatmap(this.heatmapData, modelNames, [...attackNames.map(capitalizeFirstLetter), 'Exposure score'], vendor !== ''); - }, - error: error => console.error('❌ Erreur API:', error), - }); - } - - createHeatmap(data: number[][], modelNames: Record = {}, attackNames: string[] = [], oneVendorDisplayed: boolean = false) { + // Create the heatmap chart with the processed data + createHeatmap(data: ScoreResponse, modelNames: string[] = [], attackNames: string[] = []) { const cellSize = 100; - const chartWidth = attackNames.length * cellSize + 150; // +100 to allow some space for translated labels - const chartHeight = data.length <= 3 ? data.length * cellSize + 100 : data.length * cellSize; - // const series = Object.entries(modelNames).flatMap(([vendor, models]) => - // models.map((model, modelIndex) => ({ - // name: splitModelName(vendor, model), - // data: data[modelIndex].map((value, colIndex) => ({ - // x: attackNames[colIndex], - // y: value, - // })), - // })) - // ); - - // // group by vendors - // let globalIndex = 0; - // const series = Object.entries(modelNames).flatMap(([vendor, models]) => - // models.map(model => { - // const seriesData = { - // name: splitModelName(vendor, model), - // data: data[globalIndex].map((value, colIndex) => ({ - // x: attackNames[colIndex], - // y: value, - // })), - // }; - // globalIndex++; // Increment global index for next model - // return seriesData; - // }) - // ); - - // does not group by vendor - // Flatten all models but keep vendor info - const allModels = Object.entries(modelNames).flatMap(([vendor, models]) => models.map(model => ({vendor, model}))); - - let globalIndex = 0; - - const series = allModels.map(({vendor, model}) => { + const chartWidth = (attackNames.length + 1) * cellSize + 200; // +1 to add exposure column +200 to allow some space for translated labels + const chartHeight = data.models.length <= 3 ? data.models.length * cellSize + 300 : data.models.length * cellSize; + let allModels: any[] = []; // Initialize an empty array to hold all results + const xaxisCategories = [...attackNames, 'Exposure score']; + + // Build a lookup for attack weights + const attackWeights: Record = {}; + data.attacks.forEach(attack => { + attackWeights[attack.name] = attack.weight ?? 1; // default weight to 1 if undefined + }); + + data.models.forEach(model => { + // Copy scores to avoid mutating the original object + const standalone_scores = structuredClone(model.scores); + + // Get PromptMap scores to be computed together + const pm_scores = (model.scores['promptmap-SPL'] ?? 0) + (model.scores['promptmap-PI'] ?? 0); + // Clean up PromptMap scores to avoid double counting them + delete standalone_scores['promptmap-SPL']; + delete standalone_scores['promptmap-PI']; + + // Get PromptMap weights to be computed together + const pm_weight = (attackWeights['promptmap-SPL'] ?? 0) + (attackWeights['promptmap-PI'] ?? 0); + + // Get attack names and scores + const weights = attackNames.map(name => attackWeights[name] ?? 0); + const scores = attackNames.map(name => standalone_scores[name] ?? 0); + + // Calculate exposure score + const exposureScore = (() => { + const totalWeight = weights.reduce((a, b) => a + b, 0); + if (totalWeight === 0) return 0; + const weightedSum = + pm_scores * pm_weight + + scores.reduce((sum, score, i) => sum + score * weights[i], 0); + + return Math.round(weightedSum / totalWeight); + })(); + + // Prepare the series data for the heatmap mapping attacks to models and their scores const seriesData = { - name: splitModelName(vendor, model), // Display vendor and model together - data: data[globalIndex].map((value, colIndex) => ({ - x: attackNames[colIndex], - y: value, - })), + name: model.name, + data: [ + ...attackNames.map(name => ({ + x: name, + y: model.scores[name] ?? 0, + })), + // Add exposure score manually as the last column + { + x: 'Exposure score', + y: exposureScore, + }, + ], }; - globalIndex++; // Move to next row in data - return seriesData; + allModels.push(seriesData); }); - + // Create the heatmap chart with the processed data and parameters const options = { chart: { type: 'heatmap', height: chartHeight, width: chartWidth, toolbar: {show: false}, - events: { - legendClick: function () { - console.log('CLICKED'); - }, - }, }, - series: series, + series: allModels, plotOptions: { heatmap: { shadeIntensity: 0.5, - // useFillColorAsStroke: true, // Améliore le rendu des cases colorScale: { ranges: [ - // {from: 0, to: 20, color: '#5aa812'}, // Light green for 0-20 - // {from: 21, to: 40, color: '#00A100'}, // Darker green for 21-40 - // {from: 41, to: 60, color: '#FFB200'}, // Light orange for 41-60 - // {from: 61, to: 80, color: '#FF7300'}, // Darker orange for 61-80 - // {from: 81, to: 100, color: '#FF0000'}, // Red for 81-100 - {from: 0, to: 40, color: '#00A100'}, // {from: 21, to: 40, color: '#128FD9'}, {from: 41, to: 80, color: '#FF7300'}, @@ -196,50 +137,90 @@ export class HeatmapComponent implements AfterViewInit, OnInit { }, }, grid: { - padding: {top: 0, right: 0, bottom: 0, left: 0}, + // Add padding to the top so we can space the x-axis title + padding: {top: 30, right: 0, bottom: 0, left: 0}, }, dataLabels: { - style: {fontSize: '14px'}, + style: { + // Size of the numbers in the cells + fontSize: '14px' + }, }, legend: { + // Shows the colors legend of the heatmap show: true, - // markers: { - // customHTML: function () { - // return ''; - // }, - // }, - // markers: { - // width: 12, - // height: 12, - // // Remove customHTML if you want the default - // }, }, xaxis: { - categories: attackNames, - title: {text: 'Attacks'}, - labels: {rotate: -45, style: {fontSize: '12px'}}, + categories: xaxisCategories.map(capitalizeFirstLetter), + title: { + text: 'Attacks', + offsetY: -20, + }, + labels: { + rotate: -45, + style: + { + fontSize: '12px' + } + }, position: 'top', + tooltip: { + enabled: false // Disable tooltip buble above the x-axis + }, }, yaxis: { categories: modelNames, title: { text: 'Models', - offsetX: oneVendorDisplayed ? -90 : -60, + offsetX: -75, }, labels: { + formatter: function (modelName: string) { + if (typeof modelName !== 'string') { + return modelName; // Return as is when it's a number + } + const splitName = splitModelName(modelName); + return splitName + }, style: { fontSize: '12px', + whiteSpace: 'pre-line', }, offsetY: -10, }, reversed: true, }, tooltip: { - y: { - formatter: undefined, - title: { - formatter: (seriesName: string) => seriesName.replace(',', '-'), - }, + enabled: true, + custom: function({ + series, + seriesIndex, + dataPointIndex, + w + }: { + series: any[]; + seriesIndex: number; + dataPointIndex: number; + w: any; + }) { + const value = series[seriesIndex][dataPointIndex]; + const yLabel = capitalizeFirstLetter(w.globals.initialSeries[seriesIndex].name); + const xLabel = capitalizeFirstLetter(w.globals.labels[dataPointIndex]); + // Html format the tooltip content with title = model name and body = attack name and score + return ` +
+
${yLabel}
+
+
${xLabel}: ${value}
+
+ `; }, }, }; @@ -250,42 +231,4 @@ export class HeatmapComponent implements AfterViewInit, OnInit { chart.render(); } } - - public onVendorChange(event: any) { - this.loadHeatmapData(this.selectedVendor); - } - - // getattacksNames() return an array of attacks names from the server from http://localhost:3000/api/attacks - getAttacksNames(): Observable { - return this.http.get(`${environment.api_url}/api/attacks`).pipe( - // return this.http.get(`http://127.0.0.1:8080/api/attacks`).pipe( - map(data => data.map(row => row['attackName'])) // Extract only attack names - ); - } - - getWeightedAttacks(): Observable<{attackName: string; weight: string}[]> { - return this.http.get(`${environment.api_url}/api/attacks`); - // return this.http.get(`http://127.0.0.1:8080/api/attacks`); - } - - getVendors(): Observable { - this.changeDetector.detectChanges(); - return this.http.get(`${environment.api_url}/api/vendors`); - // return this.http.get(`http://127.0.0.1:8080/api/vendors`); - } - - uploadCSV(event: any) { - const file = event.target.files[0]; - const formData = new FormData(); - formData.append('file', file); - - this.http.post(`${environment.api_url}/api/upload-csv`, formData).subscribe({ - next: res => { - console.log('Upload success', res); - }, - error: err => { - console.error('Upload failed', err); - }, - }); - } } diff --git a/frontend/src/app/types/API.ts b/frontend/src/app/types/API.ts index bd52fbc..2428462 100644 --- a/frontend/src/app/types/API.ts +++ b/frontend/src/app/types/API.ts @@ -7,53 +7,66 @@ export type APIResponse = interface MessageResponse { type: "message"; - data: string; + data: string; } interface StatusResponse { type: "status"; - current: number; - total: number; + current: number; + total: number; } export type ReportItem = { - status: string; - title: string; - description: string; - progress: number; + status: string; + title: string; + description: string; + progress: number; } interface ReportResponse { type: "report"; - reset: boolean; - data: ReportItem[]; + reset: boolean; + data: ReportItem[]; } interface IntermediateResponse { type: "intermediate"; - data: string; + data: string; } // Vulnerability Reports (used for Report Cards) interface ReportDetails { - summary: string | undefined; + summary: string | undefined; } export interface AttackReport { - attack: string; - success: boolean; - vulnerability_type: string; - details: ReportDetails; + attack: string; + success: boolean; + vulnerability_type: string; + details: ReportDetails; } interface VulnerabilityReportItem { - vulnerability: string; - reports: AttackReport[]; + vulnerability: string; + reports: AttackReport[]; } interface VulnerabilityReport { type: "vulnerability-report"; - data: VulnerabilityReportItem[]; - name: string + data: VulnerabilityReportItem[]; + name: string; +} + +export interface ScoreResponse { + attacks: { + name: string; + weight: number; + }[]; + models: { + name: string; + scores: {[attackName: string]: number}; + total_attacks: number; + total_success: number; + }[]; } diff --git a/frontend/src/app/utils/utils.ts b/frontend/src/app/utils/utils.ts index 8c9b220..e6d9e3c 100644 --- a/frontend/src/app/utils/utils.ts +++ b/frontend/src/app/utils/utils.ts @@ -1,11 +1,11 @@ // export function generateModelName(vendor: string, modelType: string, version: string, specialization: string, other: string, withVendor = true): string { -export function generateModelName(data: any[], vendor: string): any { +export function generateModelName(data: any[], vendor: string): Record { const result: Record = {}; data.forEach(row => { const vendorName = vendor === '' ? row['vendor'] : vendor; // Si vendor est vide, on prend row['vendor'], sinon on utilise vendor existant - const model = [row['modelType'], row['version'], row['specialization'], row['other']] + const model = [row['modleName']] .filter(value => value) .join('-') .replace(/\s+/g, ' ') @@ -20,19 +20,20 @@ export function generateModelName(data: any[], vendor: string): any { return result; } -export function splitModelName(vendor: string, model: string): string[] { - if (model.length < 18) return [vendor, model]; // No need to split +// Function to split model names longer than 18 characters into two parts to fit in the ui y-axis +export function splitModelName(model: string): string[] { + if (model.length < 18) return [capitalizeFirstLetter(model)]; // No need to split // Find the last "-" before the 20th character const cutoffIndex = model.lastIndexOf('-', 20); if (cutoffIndex === -1) { // If no "-" found before 20, force split at 20 - return [vendor, model.slice(0, 20), model.slice(20)]; + return [model.slice(0, 20), model.slice(20)]; } // Split at the last "-" before 20 - return [vendor, model.slice(0, cutoffIndex), model.slice(cutoffIndex + 1)].map(capitalizeFirstLetter); + return [capitalizeFirstLetter(model.slice(0, cutoffIndex)), model.slice(cutoffIndex + 1)]; } export function capitalizeFirstLetter(str: string): string { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index c58dc05..ed4790c 100755 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,7 +1,24 @@ -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { importProvidersFrom, inject, provideAppInitializer } from '@angular/core'; -import { AppModule } from './app/app.module'; +import { AppComponent } from './app/app.component'; +import { AppRoutingModule } from './app/app-routing.module'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ConfigService } from './app/services/config.service'; +import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { MarkdownModule } from 'ngx-markdown'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MaterialModule } from './app/material.module'; +import { bootstrapApplication } from '@angular/platform-browser'; - -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +bootstrapApplication(AppComponent, { + providers: [ + importProvidersFrom(AppRoutingModule, BrowserAnimationsModule, FormsModule, HttpClientModule, MaterialModule, MarkdownModule.forRoot(), MatProgressBarModule), + ConfigService, + provideAppInitializer(() => { + const configService = inject(ConfigService); + // Return a Promise or Observable for async initialization + return configService.loadConfig(); + }), + ], +}).catch(err => console.error(err)); From 91d7f676a4fbe2bc4418ef50cafe76e170781f4f Mon Sep 17 00:00:00 2001 From: Caroline BANCHEREAU Date: Thu, 24 Jul 2025 17:22:05 +0200 Subject: [PATCH 2/2] attack_model_id -> target_model_id calculate new promptmap score --- backend-agent/app/db/models.py | 6 +- backend-agent/app/db/utils.py | 6 +- backend-agent/main.py | 2 +- frontend/src/app/heatmap/heatmap.component.ts | 72 ++++++++----------- frontend/src/app/utils/utils.ts | 22 ------ 5 files changed, 37 insertions(+), 71 deletions(-) diff --git a/backend-agent/app/db/models.py b/backend-agent/app/db/models.py index 6866936..6b571a9 100644 --- a/backend-agent/app/db/models.py +++ b/backend-agent/app/db/models.py @@ -32,7 +32,7 @@ class SubAttack(db.Model): class AttackResult(db.Model): __tablename__ = 'attack_results' id = db.Column(db.Integer, primary_key=True) - attack_model_id = db.Column(db.Integer, db.ForeignKey('target_models.id'), nullable=False) # noqa: E501 + target_model_id = db.Column(db.Integer, db.ForeignKey('target_models.id'), nullable=False) # noqa: E501 attack_id = db.Column(db.Integer, db.ForeignKey('attacks.id'), nullable=False) # noqa: E501 success = db.Column(db.Boolean, nullable=False) vulnerability_type = db.Column(db.String, nullable=True) @@ -44,13 +44,13 @@ class AttackResult(db.Model): class ModelAttackScore(db.Model): __tablename__ = 'model_attack_scores' id = db.Column(db.Integer, primary_key=True) - attack_model_id = db.Column(db.Integer, db.ForeignKey('target_models.id'), nullable=False) # noqa: E501 + target_model_id = db.Column(db.Integer, db.ForeignKey('target_models.id'), nullable=False) # noqa: E501 attack_id = db.Column(db.Integer, db.ForeignKey('attacks.id'), nullable=False) # noqa: E501 total_number_of_attack = db.Column(db.Integer, nullable=False) total_success = db.Column(db.Integer, nullable=False) __table_args__ = ( - db.UniqueConstraint('attack_model_id', 'attack_id', name='uix_model_attack'), # noqa: E501 + db.UniqueConstraint('target_model_id', 'attack_id', name='uix_model_attack'), # noqa: E501 ) diff --git a/backend-agent/app/db/utils.py b/backend-agent/app/db/utils.py index f1cc505..c94df31 100644 --- a/backend-agent/app/db/utils.py +++ b/backend-agent/app/db/utils.py @@ -51,7 +51,7 @@ def save_to_db(attack_results: AttackResultDB) -> list[AttackResultDB]: # Add the attack result to inserted_records db_record = AttackResultDB( - attack_model_id=target_model.id, + target_model_id=target_model.id, attack_id=attack.id, success=success, vulnerability_type=vulnerability_type, @@ -63,12 +63,12 @@ def save_to_db(attack_results: AttackResultDB) -> list[AttackResultDB]: # If model_attack_score does not exist, create it # otherwise, update the existing record model_attack_score = ModelAttackScoreDB.query.filter_by( - attack_model_id=target_model.id, + target_model_id=target_model.id, attack_id=attack.id ).first() if not model_attack_score: model_attack_score = ModelAttackScoreDB( - attack_model_id=target_model.id, + target_model_id=target_model.id, attack_id=attack.id, total_number_of_attack=details.get('total_attacks', 0), total_success=details.get('number_successful_attacks', 0) diff --git a/backend-agent/main.py b/backend-agent/main.py index a66116c..3a224ac 100644 --- a/backend-agent/main.py +++ b/backend-agent/main.py @@ -165,7 +165,7 @@ def get_heatmap(): Attack.name.label('attack_name'), Attack.weight.label('attack_weight') ) - .join(TargetModel, ModelAttackScore.attack_model_id == TargetModel.id) # noqa: E501 + .join(TargetModel, ModelAttackScore.target_model_id == TargetModel.id) # noqa: E501 .join(Attack, ModelAttackScore.attack_id == Attack.id) ) diff --git a/frontend/src/app/heatmap/heatmap.component.ts b/frontend/src/app/heatmap/heatmap.component.ts index 9994055..97677d2 100644 --- a/frontend/src/app/heatmap/heatmap.component.ts +++ b/frontend/src/app/heatmap/heatmap.component.ts @@ -59,59 +59,41 @@ export class HeatmapComponent implements AfterViewInit, OnInit { const cellSize = 100; const chartWidth = (attackNames.length + 1) * cellSize + 200; // +1 to add exposure column +200 to allow some space for translated labels const chartHeight = data.models.length <= 3 ? data.models.length * cellSize + 300 : data.models.length * cellSize; - let allModels: any[] = []; // Initialize an empty array to hold all results - const xaxisCategories = [...attackNames, 'Exposure score']; - // Build a lookup for attack weights - const attackWeights: Record = {}; - data.attacks.forEach(attack => { - attackWeights[attack.name] = attack.weight ?? 1; // default weight to 1 if undefined - }); - + const xaxisCategories = [...attackNames, 'Exposure score']; + const allAttackWeights = Object.fromEntries( + data.attacks.map(attack => [attack.name, attack.weight ?? 1]) + ); + let seriesData: any[] = []; + // Process each model's scores and calculate the exposure score data.models.forEach(model => { - // Copy scores to avoid mutating the original object - const standalone_scores = structuredClone(model.scores); - - // Get PromptMap scores to be computed together - const pm_scores = (model.scores['promptmap-SPL'] ?? 0) + (model.scores['promptmap-PI'] ?? 0); - // Clean up PromptMap scores to avoid double counting them - delete standalone_scores['promptmap-SPL']; - delete standalone_scores['promptmap-PI']; + let weightedSum = 0; + let totalWeight = 0; - // Get PromptMap weights to be computed together - const pm_weight = (attackWeights['promptmap-SPL'] ?? 0) + (attackWeights['promptmap-PI'] ?? 0); + attackNames.forEach(attack => { + const weight = allAttackWeights[attack] ?? 1; + const score = model.scores[attack]; - // Get attack names and scores - const weights = attackNames.map(name => attackWeights[name] ?? 0); - const scores = attackNames.map(name => standalone_scores[name] ?? 0); + if (score !== undefined && score !== null) { + weightedSum += score * weight; + totalWeight += weight; + } + }); - // Calculate exposure score - const exposureScore = (() => { - const totalWeight = weights.reduce((a, b) => a + b, 0); - if (totalWeight === 0) return 0; - const weightedSum = - pm_scores * pm_weight + - scores.reduce((sum, score, i) => sum + score * weights[i], 0); - - return Math.round(weightedSum / totalWeight); - })(); - - // Prepare the series data for the heatmap mapping attacks to models and their scores - const seriesData = { + const exposureScore = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0; + seriesData.push({ name: model.name, data: [ ...attackNames.map(name => ({ x: name, - y: model.scores[name] ?? 0, + y: model.scores.hasOwnProperty(name) ? model.scores[name] : -1, })), - // Add exposure score manually as the last column { x: 'Exposure score', y: exposureScore, }, ], - }; - allModels.push(seriesData); + }); }); // Create the heatmap chart with the processed data and parameters const options = { @@ -121,12 +103,13 @@ export class HeatmapComponent implements AfterViewInit, OnInit { width: chartWidth, toolbar: {show: false}, }, - series: allModels, + series: seriesData, plotOptions: { heatmap: { shadeIntensity: 0.5, colorScale: { ranges: [ + {from: -10, to: 0, color: '#cccccc', name: 'N/A'}, // Color for unscanned cells = '-' {from: 0, to: 40, color: '#00A100'}, // {from: 21, to: 40, color: '#128FD9'}, {from: 41, to: 80, color: '#FF7300'}, @@ -141,6 +124,10 @@ export class HeatmapComponent implements AfterViewInit, OnInit { padding: {top: 30, right: 0, bottom: 0, left: 0}, }, dataLabels: { + // Format the data labels visualized in the heatmap cells + formatter: function (val: number | null) { + return (val === null || val < 0) ? '-' : val; + }, style: { // Size of the numbers in the cells fontSize: '14px' @@ -179,8 +166,8 @@ export class HeatmapComponent implements AfterViewInit, OnInit { if (typeof modelName !== 'string') { return modelName; // Return as is when it's a number } - const splitName = splitModelName(modelName); - return splitName + const splitName = splitModelName(modelName); + return splitName }, style: { fontSize: '12px', @@ -203,7 +190,8 @@ export class HeatmapComponent implements AfterViewInit, OnInit { dataPointIndex: number; w: any; }) { - const value = series[seriesIndex][dataPointIndex]; + // Handle the case where the score is -1 (unscanned) and display 'N/A' in the tooltip + const value = series[seriesIndex][dataPointIndex] === -1 ? 'N/A' : series[seriesIndex][dataPointIndex]; const yLabel = capitalizeFirstLetter(w.globals.initialSeries[seriesIndex].name); const xLabel = capitalizeFirstLetter(w.globals.labels[dataPointIndex]); // Html format the tooltip content with title = model name and body = attack name and score diff --git a/frontend/src/app/utils/utils.ts b/frontend/src/app/utils/utils.ts index e6d9e3c..9c9139e 100644 --- a/frontend/src/app/utils/utils.ts +++ b/frontend/src/app/utils/utils.ts @@ -1,25 +1,3 @@ -// export function generateModelName(vendor: string, modelType: string, version: string, specialization: string, other: string, withVendor = true): string { -export function generateModelName(data: any[], vendor: string): Record { - const result: Record = {}; - - data.forEach(row => { - const vendorName = vendor === '' ? row['vendor'] : vendor; // Si vendor est vide, on prend row['vendor'], sinon on utilise vendor existant - - const model = [row['modleName']] - .filter(value => value) - .join('-') - .replace(/\s+/g, ' ') - .trim(); - - if (!result[vendorName]) { - result[vendorName] = []; - } - - result[vendorName].push(model); - }); - return result; -} - // Function to split model names longer than 18 characters into two parts to fit in the ui y-axis export function splitModelName(model: string): string[] { if (model.length < 18) return [capitalizeFirstLetter(model)]; // No need to split