Merge pull request 'Merge latest updates into main' (#2) from dev into main
Some checks failed
Deploy Frontend / deploy (push) Failing after 26s
Some checks failed
Deploy Frontend / deploy (push) Failing after 26s
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
certificate (5).zip
|
||||
.env.dev
|
||||
.env.prod
|
||||
|
||||
51
ENV_FILES.md
Normal file
51
ENV_FILES.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Environment Files Setup
|
||||
|
||||
## Overview
|
||||
Environment variables are stored securely in `/etc/frontend/` and accessed via symlinks in this directory.
|
||||
|
||||
## File Structure
|
||||
```
|
||||
/etc/frontend/
|
||||
├── dev.env (systemd environment for dev)
|
||||
└── prod.env (systemd environment for prod)
|
||||
|
||||
/home/sonder/ld-sysinfo-react-frontend/
|
||||
├── .env.dev -> /etc/frontend/dev.env (symlink)
|
||||
└── .env.prod -> /etc/frontend/prod.env (symlink)
|
||||
```
|
||||
|
||||
## Editing Environment Variables
|
||||
|
||||
You can edit these files directly from VSCode:
|
||||
- `.env.dev` - Development environment (port 3002)
|
||||
- `.env.prod` - Production environment (port 3001)
|
||||
|
||||
**Note:** You need to be in the `frontend-devs` group to edit these files. Both `sonder` and `deploy` users are members.
|
||||
|
||||
## After Editing
|
||||
|
||||
After making changes to environment files, restart the relevant service:
|
||||
|
||||
```bash
|
||||
# For development
|
||||
sudo systemctl restart frontend@dev
|
||||
|
||||
# For production
|
||||
sudo systemctl restart frontend@prod
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Both files contain:
|
||||
- `PORT` - Server port (3002 for dev, 3001 for prod)
|
||||
- `API_BASE_URL` - Server-side API endpoint
|
||||
- `NEXT_PUBLIC_API_BASE_URL` - Client-side API endpoint
|
||||
- `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_NAME` - Database credentials
|
||||
- `NVD_API_KEY` - API key for CVE data
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Files are stored in `/etc/frontend/` with `660` permissions (owner: root, group: frontend-devs)
|
||||
- Only root and members of `frontend-devs` group can read/write
|
||||
- Files are **not** committed to git (listed in `.gitignore`)
|
||||
- Symlinks are safe to commit as they don't contain sensitive data
|
||||
221
package-lock.json
generated
221
package-lock.json
generated
@@ -19,8 +19,10 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"html2canvas": "^1.4.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonstream": "^1.0.3",
|
||||
"jspdf": "^3.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mysql2": "^3.14.0",
|
||||
"next": "15.2.4",
|
||||
@@ -36,6 +38,7 @@
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jspdf": "^1.3.3",
|
||||
"@types/jwt-decode": "^3.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
@@ -2474,6 +2477,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jspdf": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.3.3.tgz",
|
||||
"integrity": "sha512-DqwyAKpVuv+7DniCp2Deq1xGvfdnKSNgl9Agun2w6dFvR5UKamiv4VfYUgcypd8S9ojUyARFIlZqBrYrBMQlew==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jwt-decode": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jwt-decode/-/jwt-decode-3.1.0.tgz",
|
||||
@@ -2496,6 +2506,12 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
@@ -2508,6 +2524,13 @@
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
|
||||
@@ -2546,6 +2569,13 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz",
|
||||
@@ -3543,6 +3573,15 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -3688,6 +3727,33 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3822,6 +3888,18 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
|
||||
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@@ -3885,6 +3963,15 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -4091,6 +4178,16 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
|
||||
"integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
@@ -4847,6 +4944,17 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
@@ -4873,6 +4981,12 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
@@ -5316,6 +5430,19 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -5388,6 +5515,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -5948,6 +6081,23 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz",
|
||||
"integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.9",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -6854,6 +7004,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -6931,6 +7087,13 @@
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -7085,6 +7248,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
@@ -7337,6 +7510,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -7707,6 +7890,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/stream": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz",
|
||||
@@ -7922,6 +8115,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.0.tgz",
|
||||
@@ -7937,6 +8140,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
@@ -8358,6 +8570,15 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"html2canvas": "^1.4.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonstream": "^1.0.3",
|
||||
"jspdf": "^3.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mysql2": "^3.14.0",
|
||||
"next": "15.2.4",
|
||||
@@ -40,6 +42,7 @@
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jspdf": "^1.3.3",
|
||||
"@types/jwt-decode": "^3.1.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
|
||||
@@ -13,6 +13,7 @@ interface UserDTO {
|
||||
email: string;
|
||||
role: string;
|
||||
clientName: string;
|
||||
clientId?: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
484
src/app/(protected)/reporting/page.tsx
Normal file
484
src/app/(protected)/reporting/page.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Chip,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PictureAsPdf as PdfIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
Security as SecurityIcon,
|
||||
Devices as DevicesIcon,
|
||||
Apps as AppsIcon
|
||||
} from '@mui/icons-material';
|
||||
import api from '@/lib/axios';
|
||||
import jsPDF from 'jspdf';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface ComplianceSummary {
|
||||
totalDevices: number;
|
||||
vulnerableDevices: number;
|
||||
totalVulnerabilities: number;
|
||||
criticalVulns: number;
|
||||
highVulns: number;
|
||||
mediumVulns: number;
|
||||
lowVulns: number;
|
||||
totalSoftware: number;
|
||||
vulnerableSoftware: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface TopVulnerability {
|
||||
cveId: string;
|
||||
title: string;
|
||||
severity: string;
|
||||
score: number;
|
||||
affectedDevices: number;
|
||||
}
|
||||
|
||||
interface TopVulnerableSoftware {
|
||||
softwareName: string;
|
||||
totalInstances: number;
|
||||
vulnerableInstances: number;
|
||||
totalCves: number;
|
||||
}
|
||||
|
||||
export default function ReportingPage() {
|
||||
const [summary, setSummary] = useState<ComplianceSummary | null>(null);
|
||||
const [topVulns, setTopVulns] = useState<TopVulnerability[]>([]);
|
||||
const [topSoftware, setTopSoftware] = useState<TopVulnerableSoftware[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const reportRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchReportingData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch compliance summary data (you'll need to create these endpoints)
|
||||
const [summaryRes, vulnsRes, softwareRes] = await Promise.all([
|
||||
api.get('/reporting/compliance-summary'),
|
||||
api.get('/reporting/top-vulnerabilities'),
|
||||
api.get('/reporting/vulnerable-software')
|
||||
]);
|
||||
|
||||
setSummary(summaryRes.data);
|
||||
setTopVulns(vulnsRes.data);
|
||||
setTopSoftware(softwareRes.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch reporting data:', error);
|
||||
// Create mock data for now since endpoints don't exist yet
|
||||
createMockData();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchReportingData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const createMockData = () => {
|
||||
setSummary({
|
||||
totalDevices: 127,
|
||||
vulnerableDevices: 89,
|
||||
totalVulnerabilities: 342,
|
||||
criticalVulns: 23,
|
||||
highVulns: 67,
|
||||
mediumVulns: 156,
|
||||
lowVulns: 96,
|
||||
totalSoftware: 1247,
|
||||
vulnerableSoftware: 89,
|
||||
lastUpdated: new Date().toISOString()
|
||||
});
|
||||
|
||||
setTopVulns([
|
||||
{ cveId: 'CVE-2024-12345', title: 'Critical Remote Code Execution', severity: 'CRITICAL', score: 9.8, affectedDevices: 34 },
|
||||
{ cveId: 'CVE-2024-12346', title: 'Privilege Escalation Vulnerability', severity: 'HIGH', score: 8.4, affectedDevices: 28 },
|
||||
{ cveId: 'CVE-2024-12347', title: 'Information Disclosure', severity: 'HIGH', score: 7.9, affectedDevices: 22 },
|
||||
{ cveId: 'CVE-2024-12348', title: 'Cross-Site Scripting', severity: 'MEDIUM', score: 6.1, affectedDevices: 19 },
|
||||
{ cveId: 'CVE-2024-12349', title: 'Denial of Service', severity: 'MEDIUM', score: 5.8, affectedDevices: 15 }
|
||||
]);
|
||||
|
||||
setTopSoftware([
|
||||
{ softwareName: 'Apache HTTP Server', totalInstances: 45, vulnerableInstances: 23, totalCves: 12 },
|
||||
{ softwareName: 'OpenSSL', totalInstances: 89, vulnerableInstances: 34, totalCves: 8 },
|
||||
{ softwareName: 'Microsoft Office', totalInstances: 127, vulnerableInstances: 67, totalCves: 15 },
|
||||
{ softwareName: 'Adobe Reader', totalInstances: 98, vulnerableInstances: 45, totalCves: 9 },
|
||||
{ softwareName: 'Java Runtime Environment', totalInstances: 76, vulnerableInstances: 28, totalCves: 6 }
|
||||
]);
|
||||
};
|
||||
|
||||
const exportToPDF = async () => {
|
||||
if (!reportRef.current || !summary) return;
|
||||
|
||||
try {
|
||||
setExporting(true);
|
||||
|
||||
// Create a new jsPDF instance
|
||||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
// Add header
|
||||
pdf.setFontSize(20);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('Compliance Security Report', pageWidth / 2, 20, { align: 'center' });
|
||||
|
||||
pdf.setFontSize(12);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
pdf.text(`Generated: ${format(new Date(), 'PPP')}`, pageWidth / 2, 30, { align: 'center' });
|
||||
|
||||
let yPosition = 45;
|
||||
|
||||
// Executive Summary
|
||||
pdf.setFontSize(16);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('Executive Summary', 20, yPosition);
|
||||
yPosition += 10;
|
||||
|
||||
pdf.setFontSize(10);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
|
||||
const summaryText = [
|
||||
`Total Devices: ${summary.totalDevices}`,
|
||||
`Vulnerable Devices: ${summary.vulnerableDevices} (${((summary.vulnerableDevices / summary.totalDevices) * 100).toFixed(1)}%)`,
|
||||
`Total Vulnerabilities: ${summary.totalVulnerabilities}`,
|
||||
`Critical: ${summary.criticalVulns} | High: ${summary.highVulns} | Medium: ${summary.mediumVulns} | Low: ${summary.lowVulns}`,
|
||||
`Software Applications: ${summary.totalSoftware}`,
|
||||
`Vulnerable Software: ${summary.vulnerableSoftware}`
|
||||
];
|
||||
|
||||
summaryText.forEach(text => {
|
||||
pdf.text(text, 20, yPosition);
|
||||
yPosition += 6;
|
||||
});
|
||||
|
||||
yPosition += 10;
|
||||
|
||||
// Top Vulnerabilities
|
||||
pdf.setFontSize(16);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('Top Critical Vulnerabilities', 20, yPosition);
|
||||
yPosition += 10;
|
||||
|
||||
pdf.setFontSize(9);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
|
||||
topVulns.slice(0, 10).forEach((vuln, index) => {
|
||||
if (yPosition > pageHeight - 30) {
|
||||
pdf.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
pdf.text(`${index + 1}. ${vuln.cveId} - ${vuln.title}`, 20, yPosition);
|
||||
yPosition += 5;
|
||||
pdf.text(` Severity: ${vuln.severity} | Score: ${vuln.score} | Affected: ${vuln.affectedDevices} devices`, 20, yPosition);
|
||||
yPosition += 8;
|
||||
});
|
||||
|
||||
// Add page break if needed
|
||||
if (yPosition > pageHeight - 80) {
|
||||
pdf.addPage();
|
||||
yPosition = 20;
|
||||
} else {
|
||||
yPosition += 10;
|
||||
}
|
||||
|
||||
// Top Vulnerable Software
|
||||
pdf.setFontSize(16);
|
||||
pdf.setFont('helvetica', 'bold');
|
||||
pdf.text('Most Vulnerable Software', 20, yPosition);
|
||||
yPosition += 10;
|
||||
|
||||
pdf.setFontSize(9);
|
||||
pdf.setFont('helvetica', 'normal');
|
||||
|
||||
topSoftware.slice(0, 10).forEach((software, index) => {
|
||||
if (yPosition > pageHeight - 30) {
|
||||
pdf.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
pdf.text(`${index + 1}. ${software.softwareName}`, 20, yPosition);
|
||||
yPosition += 5;
|
||||
pdf.text(` ${software.vulnerableInstances}/${software.totalInstances} instances vulnerable | ${software.totalCves} CVEs`, 20, yPosition);
|
||||
yPosition += 8;
|
||||
});
|
||||
|
||||
// Footer
|
||||
const totalPages = pdf.internal.pages.length - 1;
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pdf.setPage(i);
|
||||
pdf.setFontSize(8);
|
||||
pdf.text(`Page ${i} of ${totalPages}`, pageWidth - 30, pageHeight - 10);
|
||||
pdf.text('Confidential - Internal Use Only', 20, pageHeight - 10);
|
||||
}
|
||||
|
||||
// Save the PDF
|
||||
const filename = `compliance-report-${format(new Date(), 'yyyy-MM-dd')}.pdf`;
|
||||
pdf.save(filename);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to export PDF:', error);
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity?.toLowerCase()) {
|
||||
case 'critical': return 'error';
|
||||
case 'high': return 'warning';
|
||||
case 'medium': return 'info';
|
||||
case 'low': return 'success';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="60vh">
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
return (
|
||||
<Box p={4}>
|
||||
<Alert severity="error">Failed to load reporting data. Please try again later.</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={4} ref={reportRef}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={4}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
<AssessmentIcon sx={{ mr: 2, verticalAlign: 'middle' }} />
|
||||
Compliance Reporting
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
Comprehensive security compliance overview and vulnerability reporting
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={exporting ? <CircularProgress size={20} /> : <PdfIcon />}
|
||||
onClick={exportToPDF}
|
||||
disabled={exporting}
|
||||
size="large"
|
||||
>
|
||||
{exporting ? 'Generating...' : 'Export PDF'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Executive Summary Cards */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<DevicesIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Devices</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" color="primary">
|
||||
{summary.totalDevices}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{summary.vulnerableDevices} vulnerable ({((summary.vulnerableDevices / summary.totalDevices) * 100).toFixed(1)}%)
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<SecurityIcon color="error" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Vulnerabilities</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" color="error">
|
||||
{summary.totalVulnerabilities}
|
||||
</Typography>
|
||||
<Box display="flex" gap={1} mt={1} flexWrap="wrap">
|
||||
<Chip label={`Critical: ${summary.criticalVulns}`} color="error" size="small" />
|
||||
<Chip label={`High: ${summary.highVulns}`} color="warning" size="small" />
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<AppsIcon color="info" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Software</Typography>
|
||||
</Box>
|
||||
<Typography variant="h3" color="info">
|
||||
{summary.totalSoftware}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{summary.vulnerableSoftware} with vulnerabilities
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 3 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" mb={2}>Risk Level</Typography>
|
||||
<Typography
|
||||
variant="h3"
|
||||
color={summary.criticalVulns > 0 ? "error" : summary.highVulns > 10 ? "warning" : "success"}
|
||||
>
|
||||
{summary.criticalVulns > 0 ? "HIGH" : summary.highVulns > 10 ? "MEDIUM" : "LOW"}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Last updated: {format(new Date(summary.lastUpdated), 'PPp')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Detailed Reports */}
|
||||
<Grid container spacing={3}>
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<Paper sx={{ p: 3, height: 'fit-content' }}>
|
||||
<Typography variant="h6" mb={3}>
|
||||
<SecurityIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Top Critical Vulnerabilities
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell><strong>CVE ID</strong></TableCell>
|
||||
<TableCell><strong>Severity</strong></TableCell>
|
||||
<TableCell align="right"><strong>Score</strong></TableCell>
|
||||
<TableCell align="right"><strong>Devices</strong></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{topVulns.slice(0, 8).map((vuln) => (
|
||||
<TableRow key={vuln.cveId} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2" fontFamily="monospace">
|
||||
{vuln.cveId}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{vuln.title.length > 40 ? `${vuln.title.substring(0, 40)}...` : vuln.title}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={vuln.severity}
|
||||
color={getSeverityColor(vuln.severity) as "error" | "warning" | "info" | "success" | "default"}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography fontWeight="bold">
|
||||
{vuln.score}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{vuln.affectedDevices}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, lg: 6 }}>
|
||||
<Paper sx={{ p: 3, height: 'fit-content' }}>
|
||||
<Typography variant="h6" mb={3}>
|
||||
<AppsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
Most Vulnerable Software
|
||||
</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell><strong>Software</strong></TableCell>
|
||||
<TableCell align="center"><strong>Vulnerable</strong></TableCell>
|
||||
<TableCell align="center"><strong>CVEs</strong></TableCell>
|
||||
<TableCell align="center"><strong>Risk</strong></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{topSoftware.slice(0, 8).map((software) => {
|
||||
const riskLevel = software.vulnerableInstances / software.totalInstances;
|
||||
return (
|
||||
<TableRow key={software.softwareName} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{software.softwareName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Typography variant="body2">
|
||||
{software.vulnerableInstances}/{software.totalInstances}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
({((riskLevel) * 100).toFixed(0)}%)
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Typography fontWeight="bold" color="error">
|
||||
{software.totalCves}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Chip
|
||||
label={riskLevel > 0.5 ? "HIGH" : riskLevel > 0.2 ? "MEDIUM" : "LOW"}
|
||||
color={riskLevel > 0.5 ? "error" : riskLevel > 0.2 ? "warning" : "success"}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box mt={4}>
|
||||
<Divider />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 2, display: 'block' }}>
|
||||
This report contains confidential security information. Distribution should be limited to authorized personnel only.
|
||||
Report generated on {format(new Date(), 'PPP')} from data last updated {format(new Date(summary.lastUpdated), 'PPp')}.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
20
src/app/api/reporting/compliance-summary/route.ts
Normal file
20
src/app/api/reporting/compliance-summary/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
// Mock compliance summary data
|
||||
// In production, this would query your database for actual metrics
|
||||
const summary = {
|
||||
totalDevices: 127,
|
||||
vulnerableDevices: 89,
|
||||
totalVulnerabilities: 342,
|
||||
criticalVulns: 23,
|
||||
highVulns: 67,
|
||||
mediumVulns: 156,
|
||||
lowVulns: 96,
|
||||
totalSoftware: 1247,
|
||||
vulnerableSoftware: 89,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
|
||||
return NextResponse.json(summary);
|
||||
}
|
||||
66
src/app/api/reporting/top-vulnerabilities/route.ts
Normal file
66
src/app/api/reporting/top-vulnerabilities/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
// Mock top vulnerabilities data
|
||||
// In production, this would query your vulnerability database
|
||||
const topVulns = [
|
||||
{
|
||||
cveId: 'CVE-2024-12345',
|
||||
title: 'Critical Remote Code Execution in Apache HTTP Server',
|
||||
severity: 'CRITICAL',
|
||||
score: 9.8,
|
||||
affectedDevices: 34
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12346',
|
||||
title: 'Privilege Escalation Vulnerability in OpenSSL',
|
||||
severity: 'HIGH',
|
||||
score: 8.4,
|
||||
affectedDevices: 28
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12347',
|
||||
title: 'Information Disclosure in Microsoft Office',
|
||||
severity: 'HIGH',
|
||||
score: 7.9,
|
||||
affectedDevices: 22
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12348',
|
||||
title: 'Cross-Site Scripting in Adobe Reader',
|
||||
severity: 'MEDIUM',
|
||||
score: 6.1,
|
||||
affectedDevices: 19
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12349',
|
||||
title: 'Denial of Service in Java Runtime Environment',
|
||||
severity: 'MEDIUM',
|
||||
score: 5.8,
|
||||
affectedDevices: 15
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12350',
|
||||
title: 'Buffer Overflow in Windows Kernel',
|
||||
severity: 'HIGH',
|
||||
score: 7.5,
|
||||
affectedDevices: 12
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12351',
|
||||
title: 'SQL Injection in MySQL Client',
|
||||
severity: 'MEDIUM',
|
||||
score: 6.8,
|
||||
affectedDevices: 10
|
||||
},
|
||||
{
|
||||
cveId: 'CVE-2024-12352',
|
||||
title: 'Use After Free in Chrome Browser',
|
||||
severity: 'HIGH',
|
||||
score: 8.1,
|
||||
affectedDevices: 8
|
||||
}
|
||||
];
|
||||
|
||||
return NextResponse.json(topVulns);
|
||||
}
|
||||
70
src/app/api/reporting/vulnerable-software/route.ts
Normal file
70
src/app/api/reporting/vulnerable-software/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
// Mock vulnerable software data
|
||||
// In production, this would query your software inventory database
|
||||
const vulnerableSoftware = [
|
||||
{
|
||||
softwareName: 'Apache HTTP Server',
|
||||
totalInstances: 45,
|
||||
vulnerableInstances: 23,
|
||||
totalCves: 12
|
||||
},
|
||||
{
|
||||
softwareName: 'OpenSSL',
|
||||
totalInstances: 89,
|
||||
vulnerableInstances: 34,
|
||||
totalCves: 8
|
||||
},
|
||||
{
|
||||
softwareName: 'Microsoft Office',
|
||||
totalInstances: 127,
|
||||
vulnerableInstances: 67,
|
||||
totalCves: 15
|
||||
},
|
||||
{
|
||||
softwareName: 'Adobe Reader',
|
||||
totalInstances: 98,
|
||||
vulnerableInstances: 45,
|
||||
totalCves: 9
|
||||
},
|
||||
{
|
||||
softwareName: 'Java Runtime Environment',
|
||||
totalInstances: 76,
|
||||
vulnerableInstances: 28,
|
||||
totalCves: 6
|
||||
},
|
||||
{
|
||||
softwareName: 'Google Chrome',
|
||||
totalInstances: 112,
|
||||
vulnerableInstances: 23,
|
||||
totalCves: 4
|
||||
},
|
||||
{
|
||||
softwareName: 'Mozilla Firefox',
|
||||
totalInstances: 67,
|
||||
vulnerableInstances: 18,
|
||||
totalCves: 3
|
||||
},
|
||||
{
|
||||
softwareName: 'MySQL Server',
|
||||
totalInstances: 34,
|
||||
vulnerableInstances: 12,
|
||||
totalCves: 5
|
||||
},
|
||||
{
|
||||
softwareName: 'Python',
|
||||
totalInstances: 89,
|
||||
vulnerableInstances: 15,
|
||||
totalCves: 2
|
||||
},
|
||||
{
|
||||
softwareName: 'Node.js',
|
||||
totalInstances: 56,
|
||||
vulnerableInstances: 9,
|
||||
totalCves: 3
|
||||
}
|
||||
];
|
||||
|
||||
return NextResponse.json(vulnerableSoftware);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import SecurityIcon from '@mui/icons-material/Security';
|
||||
import DevicesIcon from '@mui/icons-material/Devices';
|
||||
import AppsIcon from '@mui/icons-material/Apps';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
@@ -123,6 +124,7 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
|
||||
{ label: 'Vulnerabilities', path: '/vulnerabilities', icon: <SecurityIcon /> },
|
||||
{ label: 'Devices', path: '/devices', icon: <DevicesIcon /> },
|
||||
{ label: 'Software', path: '/software', icon: <AppsIcon /> },
|
||||
{ label: 'Reporting', path: '/reporting', icon: <AssessmentIcon /> },
|
||||
];
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
// src/components/admin/UserTableSection.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Typography, Table, TableBody, TableCell, TableContainer, TableHead,
|
||||
TableRow, Paper, Box, Button, Dialog, DialogTitle, DialogContent, DialogActions
|
||||
TableRow, Paper, Box, Button, Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
TextField, Select, MenuItem, FormControl, InputLabel, IconButton
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import UserEnableToggle from './UserEnableToggle';
|
||||
import AddUserForm from './forms/AddUserForm';
|
||||
import AddClientForm from './forms/AddClientForm';
|
||||
import InviteUserForm from './forms/InviteUserForm';
|
||||
import api from '@/lib/axios';
|
||||
|
||||
interface UserDTO {
|
||||
id: number;
|
||||
@@ -20,15 +25,97 @@ interface UserDTO {
|
||||
email: string;
|
||||
role: string;
|
||||
clientName: string;
|
||||
clientId?: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface ClientDTO {
|
||||
clientId: number;
|
||||
clientIdentifier: string;
|
||||
clientName: string;
|
||||
}
|
||||
|
||||
export default function UserTableSection({ initialUsers }: { initialUsers: UserDTO[] }) {
|
||||
const [users, setUsers] = useState<UserDTO[]>(initialUsers);
|
||||
const [openUserDialog, setOpenUserDialog] = useState(false);
|
||||
const [openClientDialog, setOpenClientDialog] = useState(false);
|
||||
const [openInviteUserDialog, setOpenInviteUserDialog] = useState(false);
|
||||
|
||||
// Edit mode state
|
||||
const [editingUserId, setEditingUserId] = useState<number | null>(null);
|
||||
const [editFormData, setEditFormData] = useState<Partial<UserDTO>>({});
|
||||
const [clients, setClients] = useState<ClientDTO[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch clients for dropdown
|
||||
useEffect(() => {
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
const res = await api.get('/auth/clients');
|
||||
setClients(res.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch clients:', err);
|
||||
}
|
||||
};
|
||||
fetchClients();
|
||||
}, []);
|
||||
|
||||
const handleEdit = (user: UserDTO) => {
|
||||
setEditingUserId(user.id);
|
||||
setEditFormData({
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
clientName: user.clientName,
|
||||
clientId: user.clientId
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingUserId(null);
|
||||
setEditFormData({});
|
||||
};
|
||||
|
||||
const handleSave = async (userId: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Find the selected client
|
||||
const selectedClient = clients.find(c => c.clientId === editFormData.clientId);
|
||||
|
||||
await api.put(`/admin/users/${userId}`, {
|
||||
username: editFormData.username,
|
||||
displayName: editFormData.displayName,
|
||||
firstName: editFormData.firstName,
|
||||
lastName: editFormData.lastName,
|
||||
email: editFormData.email,
|
||||
role: editFormData.role,
|
||||
clientId: editFormData.clientId
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setUsers(users.map(user =>
|
||||
user.id === userId
|
||||
? {
|
||||
...user,
|
||||
...editFormData,
|
||||
clientName: selectedClient?.clientName || user.clientName
|
||||
}
|
||||
: user
|
||||
));
|
||||
|
||||
setEditingUserId(null);
|
||||
setEditFormData({});
|
||||
} catch (err) {
|
||||
console.error('Failed to update user:', err);
|
||||
alert('Failed to update user. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>User Management</Typography>
|
||||
@@ -57,23 +144,143 @@ export default function UserTableSection({ initialUsers }: { initialUsers: UserD
|
||||
<TableCell>Role</TableCell>
|
||||
<TableCell>Client</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.displayName}</TableCell>
|
||||
<TableCell>{user.firstName}</TableCell>
|
||||
<TableCell>{user.lastName}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.role}</TableCell>
|
||||
<TableCell>{user.clientName}</TableCell>
|
||||
<TableCell>
|
||||
<UserEnableToggle userId={user.id} initialEnabled={user.enabled} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{users.map((user) => {
|
||||
const isEditing = editingUserId === user.id;
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editFormData.username || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, username: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
user.username
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editFormData.displayName || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, displayName: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
user.displayName
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editFormData.firstName || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, firstName: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
user.firstName
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editFormData.lastName || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, lastName: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
user.lastName
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<TextField
|
||||
size="small"
|
||||
value={editFormData.email || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, email: e.target.value })}
|
||||
fullWidth
|
||||
/>
|
||||
) : (
|
||||
user.email
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={editFormData.role || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, role: e.target.value })}
|
||||
>
|
||||
<MenuItem value="USER">USER</MenuItem>
|
||||
<MenuItem value="ADMIN">ADMIN</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : (
|
||||
user.role
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
value={editFormData.clientId || ''}
|
||||
onChange={(e) => setEditFormData({ ...editFormData, clientId: Number(e.target.value) })}
|
||||
>
|
||||
{clients.map(client => (
|
||||
<MenuItem key={client.clientId} value={client.clientId}>
|
||||
{client.clientName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : (
|
||||
user.clientName
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<UserEnableToggle userId={user.id} initialEnabled={user.enabled} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isEditing ? (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleSave(user.id)}
|
||||
disabled={loading}
|
||||
size="small"
|
||||
>
|
||||
<SaveIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
color="default"
|
||||
onClick={handleCancel}
|
||||
disabled={loading}
|
||||
size="small"
|
||||
>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => handleEdit(user)}
|
||||
size="small"
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import api from '@/lib/axios';
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
||||
import api from '@/lib/axios';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { DetailedDevice } from '@/types/devices';
|
||||
import { disableConsoleInProd } from '@/lib/disableConsole';
|
||||
disableConsoleInProd();
|
||||
|
||||
// Helper function to convert UTC timestamps to local time
|
||||
const convertUtcToLocal = (utcString: string | null): string | null => {
|
||||
if (!utcString) return null;
|
||||
// Add 'Z' to indicate it's UTC, then convert to local time
|
||||
return new Date(utcString + 'Z').toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
import { DetailedDevice } from '@/types/devices';
|
||||
import { disableConsoleInProd } from '@/lib/disableConsole';
|
||||
disableConsoleInProd();
|
||||
|
||||
interface DriveInfo {
|
||||
name: string;
|
||||
@@ -32,13 +24,698 @@ interface MacAddress {
|
||||
interfaceName: string;
|
||||
macAddress: string;
|
||||
}
|
||||
|
||||
interface InstalledApp {
|
||||
app_name: string;
|
||||
app_version: string;
|
||||
publisher: string;
|
||||
}
|
||||
|
||||
|
||||
interface InstalledApp {
|
||||
app_name: string;
|
||||
app_version: string;
|
||||
publisher: string;
|
||||
}
|
||||
|
||||
type RawRecord = Record<string, unknown>;
|
||||
|
||||
const normalizeKey = (key: string): string =>
|
||||
key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
const getDirectValue = (source: RawRecord | null | undefined, keys: string[]): unknown => {
|
||||
if (!source) return undefined;
|
||||
for (const key of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
const value = source[key];
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const findValueDeep = (
|
||||
source: unknown,
|
||||
normalizedTargets: string[],
|
||||
seen = new WeakSet<object>()
|
||||
): unknown => {
|
||||
if (!source || typeof source !== 'object') return undefined;
|
||||
|
||||
const stack: unknown[] = [source];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (!current || typeof current !== 'object') continue;
|
||||
|
||||
if (seen.has(current as object)) continue;
|
||||
seen.add(current as object);
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
for (const item of current) {
|
||||
if (item && (typeof item === 'object' || typeof item === 'string')) {
|
||||
stack.push(item);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(current as RawRecord)) {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
if (normalizedTargets.includes(normalizedKey) && value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
stack.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resolveValue = (
|
||||
source: RawRecord | null | undefined,
|
||||
keys: string[],
|
||||
fallbackKeys: string[] = keys
|
||||
): unknown => {
|
||||
if (!source) return undefined;
|
||||
|
||||
const direct = getDirectValue(source, keys);
|
||||
if (direct !== undefined && direct !== null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const normalizedTargets = Array.from(new Set(fallbackKeys.map(normalizeKey))).filter(Boolean);
|
||||
return findValueDeep(source, normalizedTargets);
|
||||
};
|
||||
|
||||
const resolveArray = <T = unknown>(
|
||||
source: RawRecord | null | undefined,
|
||||
keys: string[],
|
||||
fallbackKeys?: string[]
|
||||
): T[] | undefined => {
|
||||
const value = resolveValue(source, keys, fallbackKeys ?? keys);
|
||||
if (Array.isArray(value)) {
|
||||
return value as T[];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const toOptionalString = (value: unknown): string | undefined => {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const toNonEmptyString = (value: unknown, fallback = ''): string => {
|
||||
const candidate = toOptionalString(value)?.trim();
|
||||
return candidate && candidate.length > 0 ? candidate : fallback;
|
||||
};
|
||||
|
||||
const sanitizeName = (value: string, hostname: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
if (trimmed.toLowerCase() === hostname.toLowerCase()) return '';
|
||||
if (/^[A-Z0-9_-]+$/.test(trimmed)) return '';
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const sanitizeArchitecture = (value: string): string => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || trimmed === '-1') return '';
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
const parseSizeToGB = (value: unknown): number => {
|
||||
if (value === null || value === undefined) return 0;
|
||||
|
||||
const normalizeNumeric = (num: number): number => {
|
||||
if (!Number.isFinite(num)) return 0;
|
||||
if (num > 1024 ** 3) return +(num / 1024 ** 3).toFixed(2); // bytes
|
||||
if (num > 1024 ** 2) return +(num / 1024 ** 2).toFixed(2); // KB
|
||||
if (num > 1024) return +(num / 1024).toFixed(2); // MB
|
||||
return +num.toFixed(2); // already GB
|
||||
};
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return normalizeNumeric(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return 0;
|
||||
|
||||
const numeric = parseFloat(trimmed.replace(/[^\d.\-]/g, ''));
|
||||
if (!Number.isFinite(numeric)) return 0;
|
||||
|
||||
if (/tb/i.test(trimmed)) return +(numeric * 1024).toFixed(2);
|
||||
if (/gb/i.test(trimmed)) return +numeric.toFixed(2);
|
||||
if (/mb/i.test(trimmed)) return +(numeric / 1024).toFixed(2);
|
||||
if (/kb/i.test(trimmed)) return +(numeric / (1024 ** 2)).toFixed(2);
|
||||
if (/bytes?/i.test(trimmed)) return +(numeric / (1024 ** 3)).toFixed(2);
|
||||
|
||||
return normalizeNumeric(numeric);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const normalizeDrive = (driveInput: unknown, index: number): DriveInfo | null => {
|
||||
if (!driveInput || typeof driveInput !== 'object') return null;
|
||||
const drive = driveInput as RawRecord;
|
||||
|
||||
const name = toNonEmptyString(
|
||||
resolveValue(drive, ['name', 'driveName', 'label', 'volume'], [
|
||||
'name',
|
||||
'driveName',
|
||||
'label',
|
||||
'volume',
|
||||
'drive_letter',
|
||||
'driveletter',
|
||||
'mount_point',
|
||||
'mountpoint',
|
||||
'logicaldisk',
|
||||
'deviceid',
|
||||
]) ?? `Drive ${index + 1}`,
|
||||
`Drive ${index + 1}`
|
||||
);
|
||||
const driveType = toNonEmptyString(
|
||||
resolveValue(drive, ['driveType', 'drive_type', 'type'], [
|
||||
'driveType',
|
||||
'drive_type',
|
||||
'type',
|
||||
'drivetype',
|
||||
'mediatype',
|
||||
'busType',
|
||||
]),
|
||||
'Unknown'
|
||||
);
|
||||
const totalSizeGB = parseSizeToGB(
|
||||
resolveValue(
|
||||
drive,
|
||||
['totalSizeGB', 'total_size_gb', 'totalSize', 'total_size', 'size', 'capacity', 'totalBytes', 'total_bytes'],
|
||||
[
|
||||
'totalSizeGB',
|
||||
'total_size_gb',
|
||||
'totalSize',
|
||||
'total_size',
|
||||
'size',
|
||||
'capacity',
|
||||
'totalBytes',
|
||||
'total_bytes',
|
||||
'size_bytes',
|
||||
'capacity_bytes',
|
||||
'totalsize',
|
||||
'totalspace',
|
||||
'totalspacebytes',
|
||||
]
|
||||
)
|
||||
);
|
||||
const freeSpaceGB = parseSizeToGB(
|
||||
resolveValue(
|
||||
drive,
|
||||
['freeSpaceGB', 'free_space_gb', 'freeSpace', 'free_space', 'free', 'freeBytes', 'free_bytes', 'availableBytes'],
|
||||
[
|
||||
'freeSpaceGB',
|
||||
'free_space_gb',
|
||||
'freeSpace',
|
||||
'free_space',
|
||||
'free',
|
||||
'freeBytes',
|
||||
'free_bytes',
|
||||
'availableBytes',
|
||||
'available_bytes',
|
||||
'freespace',
|
||||
'freespacebytes',
|
||||
'remaining',
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
if (!name && !totalSizeGB && !freeSpaceGB) return null;
|
||||
|
||||
return {
|
||||
name,
|
||||
driveType,
|
||||
totalSizeGB,
|
||||
freeSpaceGB,
|
||||
};
|
||||
};
|
||||
|
||||
const extractDrives = (rawInput: unknown): DriveInfo[] => {
|
||||
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||
const raw = rawInput as RawRecord;
|
||||
|
||||
const potentialArrays = [
|
||||
resolveArray(raw, ['drives']),
|
||||
resolveArray(raw, ['driveInfo']),
|
||||
resolveArray(raw, ['drive_info']),
|
||||
resolveArray(raw, ['storageDevices']),
|
||||
resolveArray(raw, ['drive_details']),
|
||||
resolveArray(raw, ['storage']),
|
||||
resolveArray(raw, ['logicalDrives']),
|
||||
resolveArray(raw, ['logical_drives']),
|
||||
resolveArray(raw, ['volumes']),
|
||||
resolveArray(raw, ['volume_info']),
|
||||
resolveArray(raw, ['disks']),
|
||||
];
|
||||
|
||||
for (const candidate of potentialArrays) {
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate
|
||||
.map((drive, idx: number) => normalizeDrive(drive, idx))
|
||||
.filter((drive): drive is DriveInfo => Boolean(drive));
|
||||
}
|
||||
}
|
||||
|
||||
const aggregatedDrive = {
|
||||
totalSize: resolveValue(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage'], [
|
||||
'total_disk_space',
|
||||
'totalDiskSpace',
|
||||
'total_storage',
|
||||
'totalSpace',
|
||||
'total_space',
|
||||
'storage_total',
|
||||
'disk_total',
|
||||
]),
|
||||
freeSpace: resolveValue(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage'], [
|
||||
'free_disk_space',
|
||||
'freeDiskSpace',
|
||||
'available_storage',
|
||||
'free_storage',
|
||||
'freeSpace',
|
||||
'free_space',
|
||||
'availableSpace',
|
||||
'available_space',
|
||||
'storage_free',
|
||||
]),
|
||||
};
|
||||
|
||||
if (aggregatedDrive.totalSize || aggregatedDrive.freeSpace) {
|
||||
const normalized = normalizeDrive(
|
||||
{
|
||||
name: 'System',
|
||||
driveType: 'Unknown',
|
||||
totalSizeGB: aggregatedDrive.totalSize,
|
||||
freeSpaceGB: aggregatedDrive.freeSpace,
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
return normalized ? [normalized] : [];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const normalizeIpAddress = (entryInput: unknown, index: number): IpAddress | null => {
|
||||
if (!entryInput || typeof entryInput !== 'object') return null;
|
||||
const entry = entryInput as RawRecord;
|
||||
|
||||
const ipAddress = toNonEmptyString(
|
||||
resolveValue(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6'], [
|
||||
'ipAddress',
|
||||
'ip_address',
|
||||
'address',
|
||||
'ip',
|
||||
'ipv4',
|
||||
'ipv6',
|
||||
'primary_ip',
|
||||
'primaryIp',
|
||||
'lan_ip',
|
||||
]) ?? '',
|
||||
''
|
||||
);
|
||||
|
||||
const interfaceName = toNonEmptyString(
|
||||
resolveValue(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic'], [
|
||||
'interfaceName',
|
||||
'interface_name',
|
||||
'adapter',
|
||||
'name',
|
||||
'nic',
|
||||
'network_adapter',
|
||||
'interface',
|
||||
'description',
|
||||
]) ?? '',
|
||||
ipAddress ? `Interface ${index + 1}` : ''
|
||||
);
|
||||
|
||||
if (!ipAddress && !interfaceName) return null;
|
||||
|
||||
const macAddress = toOptionalString(
|
||||
resolveValue(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress'], [
|
||||
'macAddress',
|
||||
'mac_address',
|
||||
'mac',
|
||||
'physicalAddress',
|
||||
'physical_address',
|
||||
'hw_address',
|
||||
])
|
||||
);
|
||||
|
||||
return {
|
||||
interfaceName: interfaceName || `Interface ${index + 1}`,
|
||||
ipAddress: ipAddress || 'Unknown',
|
||||
macAddress: macAddress?.trim() || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const extractIpAddresses = (rawInput: unknown): IpAddress[] => {
|
||||
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||
const raw = rawInput as RawRecord;
|
||||
|
||||
const potentialArrays = [
|
||||
resolveArray(raw, ['ipAddresses']),
|
||||
resolveArray(raw, ['ip_addresses']),
|
||||
resolveArray(raw, ['networkInterfaces']),
|
||||
resolveArray(raw, ['network_interfaces']),
|
||||
resolveArray(raw, ['interfaces']),
|
||||
resolveArray(raw, ['networkAdapters']),
|
||||
resolveArray(raw, ['network_adapters']),
|
||||
resolveArray(raw, ['network']),
|
||||
];
|
||||
|
||||
for (const candidate of potentialArrays) {
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate
|
||||
.map((entry, idx: number) => normalizeIpAddress(entry, idx))
|
||||
.filter((entry): entry is IpAddress => Boolean(entry));
|
||||
}
|
||||
}
|
||||
|
||||
const singleIp = resolveValue(raw, ['ipAddress', 'ip_address', 'primary_ip', 'primaryIp']);
|
||||
if (singleIp) {
|
||||
return [
|
||||
normalizeIpAddress(
|
||||
{
|
||||
ipAddress: singleIp,
|
||||
interfaceName: resolveValue(raw, ['interfaceName', 'interface_name', 'adapter']),
|
||||
macAddress: resolveValue(raw, ['macAddress', 'mac_address']),
|
||||
},
|
||||
0
|
||||
) as IpAddress,
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const normalizeInstalledApp = (appInput: unknown): InstalledApp | null => {
|
||||
if (!appInput || typeof appInput !== 'object') return null;
|
||||
const app = appInput as RawRecord;
|
||||
|
||||
const appName = toNonEmptyString(
|
||||
resolveValue(app, ['app_name', 'name', 'title'], [
|
||||
'app_name',
|
||||
'name',
|
||||
'title',
|
||||
'display_name',
|
||||
'product_name',
|
||||
]) ?? '',
|
||||
''
|
||||
);
|
||||
if (!appName) return null;
|
||||
|
||||
return {
|
||||
app_name: appName,
|
||||
app_version: toNonEmptyString(
|
||||
resolveValue(app, ['app_version', 'version'], ['app_version', 'version', 'display_version', 'product_version']) ?? '',
|
||||
''
|
||||
),
|
||||
publisher: toNonEmptyString(
|
||||
resolveValue(app, ['publisher', 'manufacturer', 'vendor'], [
|
||||
'publisher',
|
||||
'manufacturer',
|
||||
'vendor',
|
||||
'company',
|
||||
'publisher_name',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const extractInstalledApps = (rawInput: unknown): InstalledApp[] => {
|
||||
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||
const raw = rawInput as RawRecord;
|
||||
|
||||
const potentialArrays = [
|
||||
resolveArray(raw, ['installedApplications']),
|
||||
resolveArray(raw, ['installed_applications']),
|
||||
resolveArray(raw, ['software']),
|
||||
resolveArray(raw, ['applications']),
|
||||
resolveArray(raw, ['apps']),
|
||||
resolveArray(raw, ['installedPrograms']),
|
||||
resolveArray(raw, ['installed_programs']),
|
||||
resolveArray(raw, ['programs']),
|
||||
];
|
||||
|
||||
for (const candidate of potentialArrays) {
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate
|
||||
.map((app) => normalizeInstalledApp(app))
|
||||
.filter((app): app is InstalledApp => Boolean(app));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const extractMacAddresses = (rawInput: unknown, ipAddresses: IpAddress[]): MacAddress[] => {
|
||||
if (!rawInput || typeof rawInput !== 'object') return [];
|
||||
const raw = rawInput as RawRecord;
|
||||
const potentialArrays = resolveArray(raw, ['macAddresses', 'mac_addresses', 'macs', 'mac_list']);
|
||||
|
||||
if (Array.isArray(potentialArrays)) {
|
||||
return potentialArrays
|
||||
.map((entry, idx: number) => {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
const record = entry as RawRecord;
|
||||
const macAddress = toOptionalString(
|
||||
resolveValue(record, ['macAddress', 'mac_address', 'mac'], [
|
||||
'macAddress',
|
||||
'mac_address',
|
||||
'mac',
|
||||
'physicalAddress',
|
||||
'hw_address',
|
||||
])
|
||||
);
|
||||
if (!macAddress) return null;
|
||||
return {
|
||||
interfaceName: toNonEmptyString(
|
||||
resolveValue(record, ['interfaceName', 'interface_name', 'adapter'], [
|
||||
'interfaceName',
|
||||
'interface_name',
|
||||
'adapter',
|
||||
'network_adapter',
|
||||
'name',
|
||||
]) ?? `Interface ${idx + 1}`,
|
||||
`Interface ${idx + 1}`
|
||||
),
|
||||
macAddress: macAddress.trim(),
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is MacAddress => Boolean(entry));
|
||||
}
|
||||
|
||||
return ipAddresses
|
||||
.filter((entry) => Boolean(entry.macAddress))
|
||||
.map((entry) => ({
|
||||
interfaceName: entry.interfaceName,
|
||||
macAddress: entry.macAddress as string,
|
||||
}));
|
||||
};
|
||||
|
||||
const splitGpuNames = (value: unknown): string[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => toNonEmptyString(item, ''))
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
.split(/[,;\n]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const normalizeDevice = (rawInput: unknown): DetailedDevice => {
|
||||
const raw: RawRecord =
|
||||
rawInput && typeof rawInput === 'object' ? (rawInput as RawRecord) : {};
|
||||
|
||||
const deviceId = Number(
|
||||
resolveValue(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk'], [
|
||||
'deviceId',
|
||||
'device_id',
|
||||
'id',
|
||||
'deviceID',
|
||||
'device_id_pk',
|
||||
]) ?? 0
|
||||
);
|
||||
|
||||
const drives = extractDrives(raw);
|
||||
const ipAddresses = extractIpAddresses(raw);
|
||||
const macAddresses = extractMacAddresses(raw, ipAddresses);
|
||||
const clientEntry = resolveValue(raw, ['client'], ['client', 'clientInfo', 'customer']);
|
||||
const clientNameFromNested =
|
||||
clientEntry && typeof clientEntry === 'object'
|
||||
? resolveValue(clientEntry as RawRecord, ['name', 'clientName', 'client_name'])
|
||||
: undefined;
|
||||
|
||||
const hostname = toNonEmptyString(
|
||||
resolveValue(raw, ['hostname', 'hostName', 'computerName', 'deviceName'], [
|
||||
'hostname',
|
||||
'hostName',
|
||||
'computerName',
|
||||
'deviceName',
|
||||
'host',
|
||||
'machineName',
|
||||
]) ?? `Device ${deviceId}`,
|
||||
deviceId ? `Device ${deviceId}` : 'Unknown Device'
|
||||
);
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
hostname,
|
||||
osName: sanitizeName(
|
||||
toNonEmptyString(
|
||||
resolveValue(
|
||||
raw,
|
||||
['osName', 'os_name', 'operatingSystem', 'os'],
|
||||
[
|
||||
'osName',
|
||||
'os_name',
|
||||
'operatingSystem',
|
||||
'operating_system',
|
||||
'operatingSystemName',
|
||||
'operating_system_name',
|
||||
'osFriendlyName',
|
||||
'os_caption',
|
||||
'oscaption',
|
||||
]
|
||||
) ?? '',
|
||||
''
|
||||
),
|
||||
hostname
|
||||
),
|
||||
osVersion: toNonEmptyString(
|
||||
resolveValue(raw, ['osVersion', 'os_version', 'osVersionString'], [
|
||||
'osVersion',
|
||||
'os_version',
|
||||
'osVersionString',
|
||||
'operatingSystemVersion',
|
||||
'osversion',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
windowsVersion: toNonEmptyString(
|
||||
resolveValue(raw, ['windowsVersion', 'windows_version', 'osRelease'], [
|
||||
'windowsVersion',
|
||||
'windows_version',
|
||||
'osRelease',
|
||||
'windowsRelease',
|
||||
'windows_release',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
windowsBuild: toNonEmptyString(
|
||||
resolveValue(raw, ['windowsBuild', 'windows_build', 'buildNumber'], [
|
||||
'windowsBuild',
|
||||
'windows_build',
|
||||
'buildNumber',
|
||||
'build_number',
|
||||
'windowsBuildNumber',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
osArchitecture: sanitizeArchitecture(
|
||||
toNonEmptyString(
|
||||
resolveValue(raw, ['osArchitecture', 'os_architecture', 'architecture'], [
|
||||
'osArchitecture',
|
||||
'os_architecture',
|
||||
'architecture',
|
||||
'platform',
|
||||
'osarch',
|
||||
]) ?? '',
|
||||
''
|
||||
)
|
||||
),
|
||||
processorName: toNonEmptyString(
|
||||
resolveValue(raw, ['processorName', 'processor_name', 'cpuName'], [
|
||||
'processorName',
|
||||
'processor_name',
|
||||
'cpuName',
|
||||
'cpu_name',
|
||||
'processor',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
processorArchitecture: toNonEmptyString(
|
||||
resolveValue(raw, ['processorArchitecture', 'processor_architecture', 'cpuArchitecture'], [
|
||||
'processorArchitecture',
|
||||
'processor_architecture',
|
||||
'cpuArchitecture',
|
||||
'cpu_architecture',
|
||||
'cpuArch',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
gpuNames: splitGpuNames(
|
||||
resolveValue(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus', 'graphics']) ?? []
|
||||
),
|
||||
totalMemory: toNonEmptyString(
|
||||
resolveValue(
|
||||
raw,
|
||||
['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB'],
|
||||
[
|
||||
'totalMemory',
|
||||
'total_memory',
|
||||
'memory',
|
||||
'totalMemoryMb',
|
||||
'totalMemoryMB',
|
||||
'memory_total',
|
||||
'ram_total',
|
||||
'physicalMemory',
|
||||
]
|
||||
) ?? '',
|
||||
''
|
||||
),
|
||||
lastBootTime: toNonEmptyString(
|
||||
resolveValue(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime'], [
|
||||
'lastBootTime',
|
||||
'last_boot_time',
|
||||
'bootTime',
|
||||
'lastBoot',
|
||||
'lastBootUpTime',
|
||||
'last_boot',
|
||||
'boot_time',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
lastCheckedIn: toNonEmptyString(
|
||||
resolveValue(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at'], [
|
||||
'lastCheckedIn',
|
||||
'last_checked_in',
|
||||
'lastSeen',
|
||||
'checked_in_at',
|
||||
'lastSeenAt',
|
||||
'last_seen',
|
||||
]) ?? '',
|
||||
''
|
||||
),
|
||||
drives,
|
||||
ipAddresses,
|
||||
macAddresses,
|
||||
installedApplications: extractInstalledApps(raw),
|
||||
clientName:
|
||||
toOptionalString(
|
||||
resolveValue(raw, ['clientName', 'client_name'], ['clientName', 'client_name', 'client']) ??
|
||||
clientNameFromNested ??
|
||||
resolveValue(raw, ['clientIdentifier', 'client_identifier'])
|
||||
) ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
interface DeviceVulnerability {
|
||||
cveId: string;
|
||||
@@ -49,16 +726,21 @@ interface DeviceVulnerability {
|
||||
lastModifiedDate: string;
|
||||
}
|
||||
|
||||
interface CachedSoftwareEntry {
|
||||
id: number;
|
||||
softwareName: string;
|
||||
hostname: string;
|
||||
version: string;
|
||||
deviceId: number;
|
||||
totalCves: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface CachedSoftwareEntry {
|
||||
id: number;
|
||||
softwareName: string;
|
||||
hostname: string;
|
||||
version: string;
|
||||
deviceId: number;
|
||||
totalCves: number;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
interface RawDevicesResponse {
|
||||
devices?: unknown[];
|
||||
vulnerabilitiesByDevice?: { [deviceId: string]: DeviceVulnerability[] };
|
||||
}
|
||||
|
||||
interface DeviceContextType {
|
||||
devices: DetailedDevice[];
|
||||
deviceVulns: { [deviceId: string]: DeviceVulnerability[] };
|
||||
@@ -93,10 +775,12 @@ export const DeviceProvider = ({
|
||||
vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] };
|
||||
};
|
||||
}) => {
|
||||
const [devices, setDevices] = useState<DetailedDevice[]>(() => {
|
||||
if (initialData?.devices?.length) return initialData.devices;
|
||||
return [];
|
||||
});
|
||||
const [devices, setDevices] = useState<DetailedDevice[]>(() => {
|
||||
if (initialData?.devices?.length) {
|
||||
return initialData.devices.map((device) => normalizeDevice(device));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const [deviceVulns, setDeviceVulns] = useState<{ [deviceId: string]: DeviceVulnerability[] }>(
|
||||
initialData?.vulnerabilitiesByDevice ?? {}
|
||||
@@ -104,11 +788,12 @@ export const DeviceProvider = ({
|
||||
|
||||
const [detailedCveLookup, setDetailedCveLookup] = useState<{ [cveId: string]: DeviceVulnerability }>({});
|
||||
|
||||
const [cachedSoftware, setCachedSoftware] = useState<CachedSoftwareEntry[]>([]); // ⬅️ NEW
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(!initialData);
|
||||
|
||||
const { username, roles, loading: authLoading } = useAuth();
|
||||
const [cachedSoftware, setCachedSoftware] = useState<CachedSoftwareEntry[]>([]); // ⬅️ NEW
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(!initialData);
|
||||
|
||||
const { username, roles, loading: authLoading } = useAuth();
|
||||
const rawDevicesRef = useRef<unknown[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (devices.length > 0 || initialData || authLoading || !username) return;
|
||||
@@ -123,28 +808,34 @@ export const DeviceProvider = ({
|
||||
|
||||
console.group('📡 Fetching Devices, Vulnerabilities, and Software');
|
||||
|
||||
const [devicesRes, softwareRes] = await Promise.all([
|
||||
api.get(devicesEndpoint, { withCredentials: true }),
|
||||
api.get(softwareEndpoint, { withCredentials: true }),
|
||||
]);
|
||||
const [devicesRes, softwareRes] = await Promise.all([
|
||||
api.get<RawDevicesResponse>(devicesEndpoint, { withCredentials: true }),
|
||||
api.get<CachedSoftwareEntry[]>(softwareEndpoint, { withCredentials: true }),
|
||||
]);
|
||||
|
||||
console.log('✅ Devices fetched:', devicesRes.data);
|
||||
console.log('✅ Software fetched:', softwareRes.data);
|
||||
console.groupEnd();
|
||||
|
||||
// STEP 1: Set basic data (keep original UTC timestamps for calculations)
|
||||
setDevices(devicesRes.data.devices);
|
||||
setDeviceVulns(devicesRes.data.vulnerabilitiesByDevice);
|
||||
setCachedSoftware(softwareRes.data);
|
||||
const rawDevices = Array.isArray(devicesRes.data?.devices) ? (devicesRes.data.devices as unknown[]) : [];
|
||||
rawDevicesRef.current = rawDevices;
|
||||
const normalizedDevices = rawDevices.length
|
||||
? rawDevices.map((device) => normalizeDevice(device))
|
||||
: [];
|
||||
|
||||
setDevices(normalizedDevices);
|
||||
setDeviceVulns(devicesRes.data?.vulnerabilitiesByDevice ?? {});
|
||||
setCachedSoftware(softwareRes.data ?? []);
|
||||
|
||||
// STEP 2: Extract all unique CVE IDs
|
||||
const uniqueCveIds = new Set<string>();
|
||||
(Object.values(devicesRes.data.vulnerabilitiesByDevice) as DeviceVulnerability[][]).forEach((vulns) => {
|
||||
vulns.forEach((vuln) => {
|
||||
if (vuln.cveId) {
|
||||
uniqueCveIds.add(vuln.cveId);
|
||||
}
|
||||
});
|
||||
Object.values(devicesRes.data?.vulnerabilitiesByDevice ?? {}).forEach((vulns) => {
|
||||
vulns.forEach((vuln) => {
|
||||
if (vuln.cveId) {
|
||||
uniqueCveIds.add(vuln.cveId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// STEP 3: Fetch detailed CVE data
|
||||
@@ -171,35 +862,41 @@ const cveDetailsRes = await api.get<DeviceVulnerability[]>('/vuln/cves/lookup',
|
||||
setDetailedCveLookup(lookupMap);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.group('❌ Device fetch failed');
|
||||
console.error('🔴 Axios Error Message:', error.message);
|
||||
console.error('🧾 Axios Error Config:', error.config);
|
||||
if (error.response) {
|
||||
console.error('📉 Status:', error.response.status);
|
||||
console.error('📄 Response Headers:', error.response.headers);
|
||||
console.error('📄 Response Data:', error.response.data);
|
||||
} else if (error.request) {
|
||||
console.error('🛑 No response received:', error.request);
|
||||
}
|
||||
console.groupEnd();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.group('❌ Device fetch failed');
|
||||
const axiosError = error as AxiosError;
|
||||
console.error('🔴 Axios Error Message:', axiosError.message);
|
||||
console.error('🧾 Axios Error Config:', axiosError.config);
|
||||
if (axiosError.response) {
|
||||
console.error('📉 Status:', axiosError.response.status);
|
||||
console.error('📄 Response Headers:', axiosError.response.headers);
|
||||
console.error('📄 Response Data:', axiosError.response.data);
|
||||
} else if (axiosError.request) {
|
||||
console.error('🛑 No response received:', axiosError.request);
|
||||
} else {
|
||||
console.error('❓ Unexpected error:', error);
|
||||
}
|
||||
console.groupEnd();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
fetchDevicesVulnsAndSoftware();
|
||||
}, [devices.length, initialData, username, authLoading]);
|
||||
}, [devices.length, initialData, username, authLoading, roles]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("🧠 Devices updated:", devices);
|
||||
}, [devices]);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__debug_devices = devices;
|
||||
}
|
||||
useEffect(() => {
|
||||
console.log('🧠 Devices updated:', devices);
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as Window & {
|
||||
__debug_devices?: DetailedDevice[];
|
||||
__debug_rawDevices?: unknown[];
|
||||
}).__debug_devices = devices;
|
||||
(window as Window & { __debug_rawDevices?: unknown[] }).__debug_rawDevices = rawDevicesRef.current;
|
||||
}
|
||||
}, [devices]);
|
||||
|
||||
return (
|
||||
<DeviceContext.Provider value={{
|
||||
|
||||
@@ -6,10 +6,11 @@ export interface DriveInfo {
|
||||
freeSpaceGB: number;
|
||||
}
|
||||
|
||||
export interface IpAddress {
|
||||
interfaceName: string;
|
||||
ipAddress: string;
|
||||
}
|
||||
export interface IpAddress {
|
||||
interfaceName: string;
|
||||
ipAddress: string;
|
||||
macAddress?: string;
|
||||
}
|
||||
|
||||
export interface MacAddress {
|
||||
interfaceName: string;
|
||||
@@ -42,4 +43,4 @@ export interface DriveInfo {
|
||||
installedApplications: InstalledApp[];
|
||||
clientName?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user