diff --git a/package-lock.json b/package-lock.json index 41118a18e8..e1ed12a069 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "dayjs": "^1.11.19", + "dompurify": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-xml-parser": "^5.3.6", "hoist-non-react-statics": "^3.3.2", @@ -61,6 +62,7 @@ "@types/d3-cloud": "^1.2.9", "@types/d3-force": "^3.0.10", "@types/d3-hierarchy": "^3.1.7", + "@types/dompurify": "^3.0.5", "@types/node": "^25.2.3", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -224,6 +226,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1853,6 +1856,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1876,6 +1880,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3283,7 +3288,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3310,6 +3314,7 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.8.tgz", "integrity": "sha512-42sfdLZSCpsCYmLCjSuntuPcDg3PLbakSmmYfz5Auea8gZYLr+8SS5k647doVu0BRAecqYOytkX2QC5/u/8VHw==", "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", @@ -3345,6 +3350,7 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.8.tgz", "integrity": "sha512-96qygbkTjRhdkzd5HDU8fMziemN/h758/EwrFu7TlWrEP10Vw076u+Ap/sG6OT4RGPZYYoHrTlT+mkCZblWHuw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -4384,8 +4390,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4555,6 +4560,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4592,6 +4607,7 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4611,6 +4627,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4622,6 +4639,7 @@ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4651,6 +4669,13 @@ "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4702,6 +4727,7 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -5314,6 +5340,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5362,7 +5389,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -5697,6 +5723,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -5732,8 +5759,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -6345,7 +6371,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -6456,8 +6483,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -6468,6 +6494,15 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -6771,6 +6806,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6831,6 +6867,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7835,6 +7872,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -8584,6 +8622,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8818,7 +8857,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9531,6 +9569,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9559,7 +9598,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9574,7 +9612,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -9586,8 +9623,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/pretty-ms": { "version": "9.3.0", @@ -9654,6 +9690,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9665,6 +9702,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9737,6 +9775,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9965,7 +10004,8 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "peer": true }, "node_modules/redux-logger": { "version": "3.0.6", @@ -10158,6 +10198,7 @@ "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10504,7 +10545,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10515,7 +10555,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10888,8 +10927,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tiny-case": { "version": "1.0.3", @@ -10959,6 +10997,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11177,6 +11216,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11456,6 +11496,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11595,6 +11636,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11637,6 +11679,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", diff --git a/package.json b/package.json index 6a7019a68a..6e6ce99521 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "dayjs": "^1.11.19", + "dompurify": "^3.3.1", "fast-deep-equal": "^3.1.3", "fast-xml-parser": "^5.3.6", "hoist-non-react-statics": "^3.3.2", @@ -88,6 +89,7 @@ "@types/d3-cloud": "^1.2.9", "@types/d3-force": "^3.0.10", "@types/d3-hierarchy": "^3.1.7", + "@types/dompurify": "^3.0.5", "@types/node": "^25.2.3", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index 3981cac022..ea20de3819 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -175,6 +175,7 @@ "Agents by Network (Total: {{count}})": "Agenten nach Netzwerk (Gesamt: {{count}})", "Agents by Severity Class (Total: {{count}})": "Agenten nach Schweregradklasse (Gesamt: {{count}})", "Agents Filter": "Agenten-Filter", + "Agents Installation": "Agenten-Installation", "Agents successfully authorized": "Agenten erfolgreich autorisiert", "Agents successfully deleted": "Agenten erfolgreich gelöscht", "Agents successfully revoked": "Agenten erfolgreich widerrufen", @@ -482,6 +483,8 @@ "Content": "Inhalte", "Content Type": "Inhaltstyp", "Contents": "Inhalte", + "Copied!": "Kopiert", + "Copy": "Kopieren", "Copy Agent Installer checksum to clipboard": "Agent-Installationsprogramm-Prüfsumme in die Zwischenablage kopieren", "Corresponding Performance": "Zugehörige Leistungsdaten", "Corresponding Report": "Zugehöriger Bericht", @@ -494,6 +497,7 @@ "Corresponding Vulnerabilities": "Zugehörige Schwachstellen", "Could not connect to server": "Verbindung zum Server konnte nicht hergestellt werden", "Could not load dashboard settings. Reason: {{error}}": "Konnte Dashboardeinstellungen nicht laden. Grund: {{error}}", + "Could not load install instructions": "Installationsanleitung konnte nicht geladen werden", "Count": "Anzahl", "CPE": "CPE", "CPE Filter": "CPE-Filter", @@ -1686,6 +1690,7 @@ "Results with the severity \"Medium\" are currently included.": "Ergebnisse mit dem Schweregrad \"Mittel\" werden derzeit miteinbezogen.", "Resume": "Fortsetzen", "Resume Requested": "Fortsetzen angefragt", + "Retry": "Wiederholen", "Reverse Lookup Only": "Nur Invers-Lookup", "Reverse Lookup Unify": "Invers-Lookup-Vereinheitlichung", "Revision": "Revision", @@ -2150,6 +2155,7 @@ "Unique ID of the application issuing password requests (required)": "Eindeutige ID der Anwendung, die Passwort-Anfragen stellt (erforderlich)", "Unknown": "Unbekannt", "Unknown Error": "Unbekannter Fehler", + "Unknown error occurred": "Unbekannter Fehler aufgetreten", "Unknown error on login.": "Unbekannter Fehler beim Login.", "Unknown scanner type": "Unbekannter Scanner-Typ", "Unknown scanner type ({{type}})": "Unbekannter Scanner-Typ ({{type}})", diff --git a/public/locales/gsa-en.json b/public/locales/gsa-en.json index e8c24a3c5f..70a535323a 100644 --- a/public/locales/gsa-en.json +++ b/public/locales/gsa-en.json @@ -175,6 +175,7 @@ "Agents by Network (Total: {{count}})": "Agents by Network (Total: {{count}})", "Agents by Severity Class (Total: {{count}})": "Agents by Severity Class (Total: {{count}})", "Agents Filter": "Agents Filter", + "Agents Installation": "", "Agents successfully authorized": "Agents successfully authorized", "Agents successfully deleted": "Agents successfully deleted", "Agents successfully revoked": "Agents successfully revoked", @@ -482,6 +483,8 @@ "Content": "Content", "Content Type": "Content Type", "Contents": "Contents", + "Copied!": "", + "Copy": "", "Copy Agent Installer checksum to clipboard": "Copy Agent Installer checksum to clipboard", "Corresponding Performance": "Corresponding Performance", "Corresponding Report": "Corresponding Report", @@ -494,6 +497,7 @@ "Corresponding Vulnerabilities": "Corresponding Vulnerabilities", "Could not connect to server": "Could not connect to server", "Could not load dashboard settings. Reason: {{error}}": "Could not load dashboard settings. Reason: {{error}}", + "Could not load install instructions": "", "Count": "Count", "CPE": "CPE", "CPE Filter": "CPE Filter", @@ -1686,6 +1690,7 @@ "Results with the severity \"Medium\" are currently included.": "Results with the severity \"Medium\" are currently included.", "Resume": "Resume", "Resume Requested": "Resume Requested", + "Retry": "", "Reverse Lookup Only": "Reverse Lookup Only", "Reverse Lookup Unify": "Reverse Lookup Unify", "Revision": "Revision", @@ -2150,6 +2155,7 @@ "Unique ID of the application issuing password requests (required)": "Unique ID of the application issuing password requests (required)", "Unknown": "Unknown", "Unknown Error": "Unknown Error", + "Unknown error occurred": "", "Unknown error on login.": "Unknown error on login.", "Unknown scanner type": "Unknown scanner type", "Unknown scanner type ({{type}})": "Unknown scanner type ({{type}})", diff --git a/public/locales/gsa-zh_CN.json b/public/locales/gsa-zh_CN.json index 3542006540..c18a32b74a 100644 --- a/public/locales/gsa-zh_CN.json +++ b/public/locales/gsa-zh_CN.json @@ -175,6 +175,7 @@ "Agents by Network (Total: {{count}})": "按网络分类的代理(总数:{{count}})", "Agents by Severity Class (Total: {{count}})": "按严重级别分类的代理(总数:{{count}})", "Agents Filter": "代理筛选", + "Agents Installation": "代理安装", "Agents successfully authorized": "代理授权成功", "Agents successfully deleted": "代理删除成功", "Agents successfully revoked": "代理吊销成功", @@ -482,6 +483,8 @@ "Content": "内容", "Content Type": "内容类型", "Contents": "内容", + "Copied!": "已复制!", + "Copy": "复制", "Copy Agent Installer checksum to clipboard": "复制代理安装程序校验和到剪贴板", "Corresponding Performance": "此报告的性能", "Corresponding Report": "所属报告", @@ -494,6 +497,7 @@ "Corresponding Vulnerabilities": "此报告的漏洞", "Could not connect to server": "无法连接到服务器", "Could not load dashboard settings. Reason: {{error}}": "无法加载仪表板设置.原因:{{error}}", + "Could not load install instructions": "无法加载安装说明", "Count": "数量", "CPE": "CPE", "CPE Filter": "CPE筛选", @@ -1686,6 +1690,7 @@ "Results with the severity \"Medium\" are currently included.": "目前包括严重性为\"中危\"的结果.", "Resume": "继续", "Resume Requested": "恢复请求", + "Retry": "重试", "Reverse Lookup Only": "仅反向查找", "Reverse Lookup Unify": "反向查找统一", "Revision": "修订", @@ -2150,6 +2155,7 @@ "Unique ID of the application issuing password requests (required)": "发出密码请求的应用程序的唯一ID(必填)", "Unknown": "未知", "Unknown Error": "未知错误", + "Unknown error occurred": "发生未知错误", "Unknown error on login.": "登录时出现未知错误.", "Unknown scanner type": "未知扫描器类型", "Unknown scanner type ({{type}})": "未知扫描器类型({{type}})", diff --git a/public/locales/gsa-zh_TW.json b/public/locales/gsa-zh_TW.json index 3289eb48f2..7c9984505f 100644 --- a/public/locales/gsa-zh_TW.json +++ b/public/locales/gsa-zh_TW.json @@ -175,6 +175,7 @@ "Agents by Network (Total: {{count}})": "", "Agents by Severity Class (Total: {{count}})": "", "Agents Filter": "", + "Agents Installation": "代理程式安裝", "Agents successfully authorized": "", "Agents successfully deleted": "", "Agents successfully revoked": "", @@ -482,6 +483,8 @@ "Content": "內容", "Content Type": "內容類型", "Contents": "", + "Copied!": "已複製!", + "Copy": "複製", "Copy Agent Installer checksum to clipboard": "", "Corresponding Performance": "", "Corresponding Report": "", @@ -494,6 +497,7 @@ "Corresponding Vulnerabilities": "", "Could not connect to server": "", "Could not load dashboard settings. Reason: {{error}}": "", + "Could not load install instructions": "無法載入安裝說明", "Count": "", "CPE": "CPE", "CPE Filter": "", @@ -1686,6 +1690,7 @@ "Results with the severity \"Medium\" are currently included.": "", "Resume": "", "Resume Requested": "", + "Retry": "重試", "Reverse Lookup Only": "", "Reverse Lookup Unify": "", "Revision": "", @@ -2150,6 +2155,7 @@ "Unique ID of the application issuing password requests (required)": "", "Unknown": "未知", "Unknown Error": "", + "Unknown error occurred": "發生未知錯誤", "Unknown error on login.": "", "Unknown scanner type": "", "Unknown scanner type ({{type}})": "", diff --git a/src/web/Routes.tsx b/src/web/Routes.tsx index 3a7e227e60..b7693579e3 100644 --- a/src/web/Routes.tsx +++ b/src/web/Routes.tsx @@ -58,7 +58,7 @@ const loggedInRoutes = [ path: 'agent-installers', lazy: async () => ({ Component: ( - await import('web/pages/agent-installers/AgentInstallerListPage') + await import('web/pages/agent-remote-installer/AgentInstallInstructionsPage') ).default, }), }, diff --git a/src/web/pages/agent-remote-installer/AgentInstallInstructionsPage.tsx b/src/web/pages/agent-remote-installer/AgentInstallInstructionsPage.tsx new file mode 100644 index 0000000000..0f39386547 --- /dev/null +++ b/src/web/pages/agent-remote-installer/AgentInstallInstructionsPage.tsx @@ -0,0 +1,327 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + useState, + useEffect, + useCallback, + useRef, + type ChangeEvent, +} from 'react'; +import {Spinner} from '@greenbone/ui-lib'; +import DOMPurify from 'dompurify'; +import styled from 'styled-components'; +import { + type default as Scanner, + AGENT_CONTROLLER_SCANNER_TYPE, +} from 'gmp/models/scanner'; +import Layout from 'web/components/layout/Layout'; +import PageTitle from 'web/components/layout/PageTitle'; +import useGmp from 'web/hooks/useGmp'; +import useLanguage from 'web/hooks/useLanguage'; +import useTranslation from 'web/hooks/useTranslation'; + +interface AgentController { + id: string; + name: string; + host: string; + port: number; +} + +const extractStyles = (html: string): string => { + const match = html.match(/]*>([\s\S]*?)<\/style>/i); + return match ? `` : ''; +}; + +const extractBody = (html: string): string => { + const match = html.match(/]*>([\s\S]*)<\/body>/i); + return match ? match[1] : html; +}; + +const CenteredLayout = styled(Layout)` + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; +`; + +const InstructionsContainer = styled.div` + padding: 16px; + background: white; + border-radius: 8px; + width: 100%; +`; + +const SelectorBar = styled.div` + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #f5f5f5; + border-radius: 8px; + margin-bottom: 16px; +`; + +const SelectorLabel = styled.label` + font-weight: 500; + white-space: nowrap; +`; + +const SelectorSelect = styled.select` + padding: 6px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + min-width: 250px; +`; + +const ErrorContainer = styled.div` + padding: 24px; + background: #ffebee; + border: 1px solid #ef5350; + border-radius: 8px; + margin: 16px 0; +`; + +const ErrorTitle = styled.h3` + color: #c62828; + margin: 0 0 8px 0; +`; + +const ErrorMessage = styled.p` + color: #b71c1c; + margin: 0 0 16px 0; +`; + +const RetryButton = styled.button` + background: #4caf50; + color: white; + border: none; + padding: 8px 20px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + + &:hover { + background: #45a049; + } +`; + +const AgentInstallInstructionsPage = () => { + const [_] = useTranslation(); + const [language] = useLanguage(); + const gmp = useGmp(); + const [instructionsHtml, setInstructionsHtml] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [controllers, setControllers] = useState([]); + const [selectedController, setSelectedController] = useState(''); + const [controllersLoading, setControllersLoading] = useState(true); + const instructionsContainerRef = useRef(null); + + // Fetch agent-controller scanners on mount + useEffect(() => { + const fetchScanners = async () => { + try { + const response = await gmp.scanners.getAll(); + const scanners = response?.data ?? []; + const agentControllers: AgentController[] = scanners + .filter( + (s: Scanner) => + String(s.scannerType) === AGENT_CONTROLLER_SCANNER_TYPE && + s.id !== undefined && + s.name !== undefined && + s.host !== undefined, + ) + .map((s: Scanner) => ({ + id: s.id as string, + name: s.name as string, + host: s.host as string, + port: s.port ?? 443, + })) + .sort((a, b) => { + // Local agent-control first (matches docker hostname) + const aLocal = a.host === 'agentcontrol' ? 0 : 1; + const bLocal = b.host === 'agentcontrol' ? 0 : 1; + if (aLocal !== bLocal) return aLocal - bLocal; + return a.name.localeCompare(b.name); + }); + setControllers(agentControllers); + if (agentControllers.length > 0) { + setSelectedController(agentControllers[0].id); + } + } catch { + // If scanner fetch fails, fall back to local agent-control + setControllers([]); + } finally { + setControllersLoading(false); + } + }; + void fetchScanners(); + }, [gmp]); + + const getInstructionsUrl = useCallback( + (langCode: string) => { + const controller = controllers.find(c => c.id === selectedController); + const encodedLang = encodeURIComponent(langCode); + if (controller) { + // Proxy through nginx: /agent-proxy/{host}/{port}/api/v1/... + // Encode host to handle IPv6 addresses and special characters safely + const encodedHost = encodeURIComponent(controller.host); + return `/agent-proxy/${encodedHost}/${controller.port}/api/v1/install-instructions?lang=${encodedLang}`; + } + // Fallback to local agent-control + return `/api/v1/install-instructions?lang=${encodedLang}`; + }, + [controllers, selectedController], + ); + + const fetchInstructions = useCallback( + async (lang: string) => { + const langCode = lang.split(/[-_]/)[0] || 'en'; + const url = getInstructionsUrl(langCode); + + try { + setLoading(true); + setError(null); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const html = await response.text(); + const styles = extractStyles(html); + const body = extractBody(html); + // Sanitize only the body HTML to prevent XSS attacks + // Styles are kept as-is (CSS injection is low risk compared to HTML/JS) + const sanitizedBody = DOMPurify.sanitize(body, { + ADD_ATTR: ['class', 'data-clipboard-text'], + FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'style'], + FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover'], + }); + // Combine styles with sanitized body + setInstructionsHtml(styles + sanitizedBody); + } catch (err) { + setError( + err instanceof Error ? err.message : _('Unknown error occurred'), + ); + } finally { + setLoading(false); + } + }, + [_, getInstructionsUrl], + ); + + // Fetch instructions when language or selected controller changes + useEffect(() => { + if (language && !controllersLoading) { + void fetchInstructions(language); + } + }, [language, selectedController, controllersLoading, fetchInstructions]); + + // Attach click handlers to copy buttons after HTML is rendered + // Scoped to the instructions container to avoid global event handling + useEffect(() => { + const container = instructionsContainerRef.current; + if (!instructionsHtml || loading || !container) return; + + const copyToClipboard = async (btn: HTMLButtonElement) => { + const pre = btn.previousElementSibling; + if (!pre) return; + + const text = pre.textContent || ''; + const originalText = btn.textContent || _('Copy'); + const copiedText = _('Copied!'); + + try { + await navigator.clipboard.writeText(text); + } catch { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.cssText = 'position:fixed;left:-9999px'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + + btn.textContent = copiedText; + btn.classList.add('copied'); + setTimeout(() => { + btn.textContent = originalText; + btn.classList.remove('copied'); + }, 2000); + }; + + const handleClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target.classList.contains('copy-btn')) { + void copyToClipboard(target as HTMLButtonElement); + } + }; + + container.addEventListener('click', handleClick); + return () => container.removeEventListener('click', handleClick); + }, [_, instructionsHtml, loading]); + + const handleControllerChange = (e: ChangeEvent) => { + setSelectedController(e.target.value); + }; + + return ( + <> + + + {controllers.length > 1 && ( + + + {_('Agent Controller')}: + + + {controllers.map(c => ( + + ))} + + + )} + + {(loading || controllersLoading) && ( + + + + )} + + {error && !loading && ( + + {_('Could not load install instructions')} + {error} + fetchInstructions(language)}> + {_('Retry')} + + + )} + + {!loading && !controllersLoading && !error && ( + + )} + + + ); +}; + +export default AgentInstallInstructionsPage;