Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a91aa9ff | ||
|
|
bdda377482 | ||
|
|
aa11ed70bd | ||
|
|
0580dca6cf | ||
|
|
678ef0ab5a | ||
|
|
4888c27713 | ||
|
|
b256c10622 | ||
|
|
878e4e0bbc | ||
|
|
6c3653c483 | ||
|
|
71ac703e6f | ||
|
|
a89e3598f2 | ||
|
|
5d043cc929 | ||
|
|
921f94ebbf | ||
|
|
48c9ed8a79 | ||
|
|
12987ec9f9 | ||
|
|
40b90ed063 | ||
|
|
60314e781f | ||
|
|
bc56d48595 | ||
|
|
2d71cef3d5 | ||
|
|
41f6637497 | ||
|
|
c2e54bbbcc | ||
|
|
df8aacd09d | ||
|
|
2dd6be59b9 | ||
|
|
9e8e3eb0e6 | ||
|
|
3728430f8c |
@@ -24,6 +24,10 @@ POSTGRES_USER=prowler
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=prowler_db
|
||||
|
||||
# Celery-Prowler task settings
|
||||
TASK_RETRY_DELAY_SECONDS=0.1
|
||||
TASK_RETRY_ATTEMPTS=5
|
||||
|
||||
# Valkey settings
|
||||
# If running Valkey and celery on host, use localhost, else use 'valkey'
|
||||
VALKEY_HOST=valkey
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
- name: Set short git commit SHA
|
||||
id: vars
|
||||
run: |
|
||||
shortSha=$(git rev-parse --short ${{ github.sha }})
|
||||
shortSha=$(git rev-parse --short ${{ github.event.pull_request.merge_commit_sha }})
|
||||
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
|
||||
|
||||
- name: Trigger pull request
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
repository: ${{ secrets.CLOUD_DISPATCH }}
|
||||
event-type: prowler-pull-request-merged
|
||||
client-payload: '{
|
||||
"PROWLER_COMMIT_SHA": "${{ github.sha }}",
|
||||
"PROWLER_COMMIT_SHA": "${{ github.event.pull_request.merge_commit_sha }}",
|
||||
"PROWLER_COMMIT_SHORT_SHA": "${{ env.SHORT_SHA }}",
|
||||
"PROWLER_PR_TITLE": "${{ github.event.pull_request.title }}",
|
||||
"PROWLER_PR_LABELS": ${{ toJson(github.event.pull_request.labels.*.name) }},
|
||||
|
||||
@@ -86,12 +86,12 @@ prowler dashboard
|
||||
|
||||
| Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) |
|
||||
|---|---|---|---|---|
|
||||
| AWS | 564 | 82 | 35 | 10 |
|
||||
| AWS | 567 | 82 | 36 | 10 |
|
||||
| GCP | 79 | 13 | 9 | 3 |
|
||||
| Azure | 140 | 18 | 8 | 3 |
|
||||
| Azure | 142 | 18 | 10 | 3 |
|
||||
| Kubernetes | 83 | 7 | 5 | 7 |
|
||||
| GitHub | 3 | 2 | 1 | 0 |
|
||||
| M365 | 44 | 2 | 2 | 0 |
|
||||
| GitHub | 16 | 2 | 1 | 0 |
|
||||
| M365 | 69 | 7 | 2 | 2 |
|
||||
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
|
||||
|
||||
> [!Note]
|
||||
|
||||
@@ -10,12 +10,18 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
### Changed
|
||||
- Renamed field encrypted_password to password for M365 provider [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
|
||||
### Fixed
|
||||
- Fixed the connection status verification before launching a scan [(#7831)](https://github.com/prowler-cloud/prowler/pull/7831)
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
- Fixed task lookup to use task_kwargs instead of task_args for scan report resolution. [(#7830)](https://github.com/prowler-cloud/prowler/pull/7830)
|
||||
- Fixed Kubernetes UID validation to allow valid context names [(#7871)](https://github.com/prowler-cloud/prowler/pull/7871)
|
||||
- Fixed a race condition when creating background tasks [(#7876)](https://github.com/prowler-cloud/prowler/pull/7876).
|
||||
- Fixed an error when modifying or retrieving tenants due to missing user UUID in transaction context [(#7890)](https://github.com/prowler-cloud/prowler/pull/7890).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1448,18 +1448,6 @@ files = [
|
||||
graph = ["objgraph (>=1.7.2)"]
|
||||
profile = ["gprof2dot (>=2022.7.29)"]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
description = "Distro - an OS platform information API"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
|
||||
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dj-rest-auth"
|
||||
version = "7.0.1"
|
||||
@@ -2482,93 +2470,6 @@ MarkupSafe = ">=2.0"
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.10.0"
|
||||
description = "Fast iterable JSON parser."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4"},
|
||||
{file = "jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7"},
|
||||
{file = "jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01"},
|
||||
{file = "jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d"},
|
||||
{file = "jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4"},
|
||||
{file = "jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca"},
|
||||
{file = "jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070"},
|
||||
{file = "jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86"},
|
||||
{file = "jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357"},
|
||||
{file = "jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00"},
|
||||
{file = "jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf"},
|
||||
{file = "jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9"},
|
||||
{file = "jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jmespath"
|
||||
version = "1.0.1"
|
||||
@@ -3320,33 +3221,6 @@ rsa = ["cryptography (>=3.0.0)"]
|
||||
signals = ["blinker (>=1.4.0)"]
|
||||
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.82.0"
|
||||
description = "The official Python library for the openai API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "openai-1.82.0-py3-none-any.whl", hash = "sha256:8c40647fea1816516cb3de5189775b30b5f4812777e40b8768f361f232b61b30"},
|
||||
{file = "openai-1.82.0.tar.gz", hash = "sha256:b0a009b9a58662d598d07e91e4219ab4b1e3d8ba2db3f173896a92b9b874d1a7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.5.0,<5"
|
||||
distro = ">=1.7.0,<2"
|
||||
httpx = ">=0.23.0,<1"
|
||||
jiter = ">=0.4.0,<1"
|
||||
pydantic = ">=1.9.0,<3"
|
||||
sniffio = "*"
|
||||
tqdm = ">4"
|
||||
typing-extensions = ">=4.11,<5"
|
||||
|
||||
[package.extras]
|
||||
datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
|
||||
realtime = ["websockets (>=13,<16)"]
|
||||
voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.32.1"
|
||||
@@ -4763,7 +4637,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
|
||||
@@ -4772,7 +4645,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
|
||||
@@ -4781,7 +4653,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
|
||||
@@ -4790,7 +4661,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
|
||||
@@ -4799,7 +4669,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
|
||||
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
|
||||
@@ -5181,7 +5050,7 @@ version = "4.67.1"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
|
||||
{file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
|
||||
@@ -5614,4 +5483,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "bd227969f24255e5b641c79c727abe98a4aab73b2689b3e6f8cfa6fae8fa5bed"
|
||||
content-hash = "051924735a7069c8393fefc18fc2c310b196ea24ad41b8c984dc5852683d0407"
|
||||
|
||||
@@ -27,8 +27,7 @@ dependencies = [
|
||||
"psycopg2-binary==2.9.9",
|
||||
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||
"uuid6==2024.7.10",
|
||||
"openai (>=1.82.0,<2.0.0)"
|
||||
"uuid6==2024.7.10"
|
||||
]
|
||||
description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -109,16 +109,6 @@ class BaseTenantViewset(BaseViewSet):
|
||||
pass # Tenant might not exist, handle gracefully
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
if (
|
||||
request.resolver_match.url_name != "tenant-detail"
|
||||
and request.method != "DELETE"
|
||||
):
|
||||
user_id = str(request.user.id)
|
||||
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
# TODO: DRY this when we have time
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
@@ -126,8 +116,8 @@ class BaseTenantViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
user_id = str(request.user.id)
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-04-10 14:54
|
||||
import uuid
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0026_provider_secret_gcp_service_account"),
|
||||
]
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LighthouseConfig",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
max_length=100,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
),
|
||||
),
|
||||
("api_key", models.BinaryField()),
|
||||
("model", models.CharField(default="gpt-4o", max_length=50)),
|
||||
("temperature", models.FloatField(default=0.7)),
|
||||
("max_tokens", models.IntegerField(default=4000)),
|
||||
(
|
||||
"business_context",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
help_text="Additional business context for this AI model configuration",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "lighthouse_configurations",
|
||||
"abstract": False,
|
||||
"indexes": [
|
||||
models.Index(fields=["name"], name="lighthouse_config_name_idx"),
|
||||
models.Index(
|
||||
fields=["is_active"], name="lighthouse_config_active_idx"
|
||||
),
|
||||
],
|
||||
"constraints": [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id",),
|
||||
name="unique_lighthouse_config_per_tenant",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseconfig",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_lighthouseconfig",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,10 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from config.custom_logging import BackendLogger
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from config.env import env
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -51,8 +51,6 @@ fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode())
|
||||
# Convert Prowler Severity enum to Django TextChoices
|
||||
SeverityChoices = enum_to_choices(Severity)
|
||||
|
||||
logger = logging.getLogger(BackendLogger.API)
|
||||
|
||||
|
||||
class StatusChoices(models.TextChoices):
|
||||
"""
|
||||
@@ -246,7 +244,7 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
@staticmethod
|
||||
def validate_kubernetes_uid(value):
|
||||
if not re.match(
|
||||
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
|
||||
r"^[a-zA-Z0-9][a-zA-Z0-9._@:\/-]{1,250}$",
|
||||
value,
|
||||
):
|
||||
raise ModelValidationError(
|
||||
@@ -356,6 +354,42 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel):
|
||||
resource_name = "provider_groups-provider"
|
||||
|
||||
|
||||
class TaskManager(models.Manager):
|
||||
def get_with_retry(
|
||||
self,
|
||||
id: str,
|
||||
max_retries: int = None,
|
||||
delay_seconds: float = None,
|
||||
):
|
||||
"""
|
||||
Retry fetching a Task by ID in case it hasn't been created yet.
|
||||
|
||||
Args:
|
||||
id (str): The Celery task ID (expected to match Task model PK).
|
||||
max_retries (int, optional): Number of retry attempts. Defaults to env TASK_RETRY_ATTEMPTS or 5.
|
||||
delay_seconds (float, optional): Delay between retries in seconds. Defaults to env TASK_RETRY_DELAY_SECONDS or 0.1.
|
||||
|
||||
Returns:
|
||||
Task: The retrieved Task instance.
|
||||
|
||||
Raises:
|
||||
Task.DoesNotExist: If the task is not found after all retries.
|
||||
"""
|
||||
max_retries = max_retries or env.int("TASK_RETRY_ATTEMPTS", default=5)
|
||||
delay_seconds = delay_seconds or env.float(
|
||||
"TASK_RETRY_DELAY_SECONDS", default=0.1
|
||||
)
|
||||
|
||||
for _attempt in range(max_retries):
|
||||
try:
|
||||
return self.get(id=id)
|
||||
except self.model.DoesNotExist:
|
||||
time.sleep(delay_seconds)
|
||||
raise self.model.DoesNotExist(
|
||||
f"Task with ID {id} not found after {max_retries} retries."
|
||||
)
|
||||
|
||||
|
||||
class Task(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
@@ -368,6 +402,8 @@ class Task(RowLevelSecurityProtectedModel):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
objects = TaskManager()
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "tasks"
|
||||
|
||||
@@ -1336,129 +1372,3 @@ class ResourceScanSummary(RowLevelSecurityProtectedModel):
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class LighthouseConfiguration(RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Stores configuration and API keys for LLM services.
|
||||
"""
|
||||
|
||||
class ModelChoices(models.TextChoices):
|
||||
GPT_4O_2024_11_20 = "gpt-4o-2024-11-20", _("GPT-4o v2024-11-20")
|
||||
GPT_4O_2024_08_06 = "gpt-4o-2024-08-06", _("GPT-4o v2024-08-06")
|
||||
GPT_4O_2024_05_13 = "gpt-4o-2024-05-13", _("GPT-4o v2024-05-13")
|
||||
GPT_4O = "gpt-4o", _("GPT-4o Default")
|
||||
GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18", _("GPT-4o Mini v2024-07-18")
|
||||
GPT_4O_MINI = "gpt-4o-mini", _("GPT-4o Mini Default")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
validators=[MinLengthValidator(3)],
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text="Name of the configuration",
|
||||
)
|
||||
api_key = models.BinaryField(
|
||||
blank=False, null=False, help_text="Encrypted API key for the LLM service"
|
||||
)
|
||||
model = models.CharField(
|
||||
max_length=50,
|
||||
choices=ModelChoices.choices,
|
||||
blank=False,
|
||||
null=False,
|
||||
help_text="Must be one of the supported model names",
|
||||
)
|
||||
temperature = models.FloatField(default=0, help_text="Must be between 0 and 1")
|
||||
max_tokens = models.IntegerField(
|
||||
default=4000, help_text="Must be between 500 and 5000"
|
||||
)
|
||||
business_context = models.TextField(
|
||||
blank=True,
|
||||
null=False,
|
||||
default="",
|
||||
help_text="Additional business context for this AI model configuration",
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Validate temperature
|
||||
if not 0 <= self.temperature <= 1:
|
||||
raise ModelValidationError(
|
||||
detail="Temperature must be between 0 and 1",
|
||||
code="invalid_temperature",
|
||||
pointer="/data/attributes/temperature",
|
||||
)
|
||||
|
||||
# Validate max_tokens
|
||||
if not 500 <= self.max_tokens <= 5000:
|
||||
raise ModelValidationError(
|
||||
detail="Max tokens must be between 500 and 5000",
|
||||
code="invalid_max_tokens",
|
||||
pointer="/data/attributes/max_tokens",
|
||||
)
|
||||
|
||||
@property
|
||||
def api_key_decoded(self):
|
||||
"""Return the decrypted API key, or None if unavailable or invalid."""
|
||||
if not self.api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
decrypted_key = fernet.decrypt(bytes(self.api_key))
|
||||
return decrypted_key.decode()
|
||||
|
||||
except InvalidToken:
|
||||
logger.warning("Invalid token while decrypting API key.")
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error while decrypting API key: %s", e)
|
||||
|
||||
@api_key_decoded.setter
|
||||
def api_key_decoded(self, value):
|
||||
"""Store the encrypted API key."""
|
||||
if not value:
|
||||
raise ModelValidationError(
|
||||
detail="API key is required",
|
||||
code="invalid_api_key",
|
||||
pointer="/data/attributes/api_key",
|
||||
)
|
||||
|
||||
# Validate OpenAI API key format
|
||||
openai_key_pattern = r"^sk-[\w-]+T3BlbkFJ[\w-]+$"
|
||||
if not re.match(openai_key_pattern, value):
|
||||
raise ModelValidationError(
|
||||
detail="Invalid OpenAI API key format.",
|
||||
code="invalid_api_key",
|
||||
pointer="/data/attributes/api_key",
|
||||
)
|
||||
self.api_key = fernet.encrypt(value.encode())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "lighthouse_configurations"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
# Add unique constraint for name within a tenant
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id"], name="unique_lighthouse_config_per_tenant"
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "lighthouse-configuration"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import uuid
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from api.models import Resource, ResourceTag
|
||||
from api.models import Resource, ResourceTag, Task
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -120,3 +123,35 @@ class TestResourceModel:
|
||||
# compliance={},
|
||||
# )
|
||||
# assert Finding.objects.filter(uid=long_uid).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTaskManager:
|
||||
def test_get_with_retry_success(self):
|
||||
task_id = uuid.uuid4()
|
||||
call_counter = {"count": 0}
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
if call_counter["count"] < 2:
|
||||
call_counter["count"] += 1
|
||||
raise Task.DoesNotExist()
|
||||
return Task(id=task_id)
|
||||
|
||||
with mock.patch.object(Task.objects, "get", side_effect=side_effect):
|
||||
task = Task.objects.get_with_retry(
|
||||
task_id, max_retries=5, delay_seconds=0.01
|
||||
)
|
||||
|
||||
assert task.id == task_id
|
||||
assert call_counter["count"] == 2
|
||||
|
||||
def test_get_with_retry_fail(self):
|
||||
non_existent_id = uuid.uuid4()
|
||||
|
||||
with mock.patch.object(Task.objects, "get", side_effect=Task.DoesNotExist):
|
||||
with pytest.raises(Task.DoesNotExist) as excinfo:
|
||||
Task.objects.get_with_retry(
|
||||
non_existent_id, max_retries=3, delay_seconds=0.01
|
||||
)
|
||||
|
||||
assert str(non_existent_id) in str(excinfo.value)
|
||||
|
||||
@@ -914,6 +914,16 @@ class TestProviderViewSet:
|
||||
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
|
||||
"alias": "GKE",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "gke_project/cluster-name",
|
||||
"alias": "GKE",
|
||||
},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"uid": "admin@k8s-demo",
|
||||
"alias": "test",
|
||||
},
|
||||
{
|
||||
"provider": "azure",
|
||||
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
@@ -921,7 +931,7 @@ class TestProviderViewSet:
|
||||
},
|
||||
{
|
||||
"provider": "m365",
|
||||
"uid": "TestingPro.onMirosoft.com",
|
||||
"uid": "TestingPro.onmicrosoft.com",
|
||||
"alias": "test",
|
||||
},
|
||||
{
|
||||
@@ -5458,337 +5468,3 @@ class TestIntegrationViewSet:
|
||||
{f"filter[{filter_name}]": "whatever"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestLighthouseConfigViewSet:
|
||||
@pytest.fixture
|
||||
def valid_config_payload(self):
|
||||
return {
|
||||
"data": {
|
||||
"type": "lighthouse-configuration",
|
||||
"attributes": {
|
||||
"name": "OpenAI",
|
||||
"api_key": "sk-test1234567890T3BlbkFJtest1234567890",
|
||||
"model": "gpt-4o",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 4000,
|
||||
"business_context": "Test business context",
|
||||
"is_active": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_config_payload(self):
|
||||
return {
|
||||
"data": {
|
||||
"type": "lighthouse-configuration",
|
||||
"attributes": {
|
||||
"name": "T", # Too short
|
||||
"api_key": "invalid-key", # Invalid format
|
||||
"model": "invalid-model",
|
||||
"temperature": 2.0, # Invalid range
|
||||
"max_tokens": -1, # Invalid value
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
def test_lighthouse_config_list(self, authenticated_client):
|
||||
response = authenticated_client.get(reverse("lighthouseconfiguration-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"] == []
|
||||
|
||||
def test_lighthouse_config_create(self, authenticated_client, valid_config_payload):
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=valid_config_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()["data"]
|
||||
assert (
|
||||
data["attributes"]["name"]
|
||||
== valid_config_payload["data"]["attributes"]["name"]
|
||||
)
|
||||
assert (
|
||||
data["attributes"]["model"]
|
||||
== valid_config_payload["data"]["attributes"]["model"]
|
||||
)
|
||||
assert (
|
||||
data["attributes"]["temperature"]
|
||||
== valid_config_payload["data"]["attributes"]["temperature"]
|
||||
)
|
||||
assert (
|
||||
data["attributes"]["max_tokens"]
|
||||
== valid_config_payload["data"]["attributes"]["max_tokens"]
|
||||
)
|
||||
assert (
|
||||
data["attributes"]["business_context"]
|
||||
== valid_config_payload["data"]["attributes"]["business_context"]
|
||||
)
|
||||
assert (
|
||||
data["attributes"]["is_active"]
|
||||
== valid_config_payload["data"]["attributes"]["is_active"]
|
||||
)
|
||||
# Check that API key is masked with asterisks only
|
||||
masked_api_key = data["attributes"]["api_key"]
|
||||
assert all(
|
||||
c == "*" for c in masked_api_key
|
||||
), "API key should contain only asterisks"
|
||||
|
||||
def test_lighthouse_config_create_invalid_name_too_short(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
"""Test that name validation fails when too short"""
|
||||
payload = valid_config_payload.copy()
|
||||
payload["data"]["attributes"]["name"] = "T" # Too short
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert any("name" in error["source"]["pointer"] for error in errors)
|
||||
|
||||
def test_lighthouse_config_create_invalid_api_key_format(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
"""Test that API key validation fails with invalid format"""
|
||||
payload = valid_config_payload.copy()
|
||||
payload["data"]["attributes"]["api_key"] = "invalid-key"
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert any(
|
||||
"Invalid OpenAI API key format." in error["detail"] for error in errors
|
||||
)
|
||||
|
||||
def test_lighthouse_config_create_invalid_model(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
"""Test that model validation fails with invalid model name"""
|
||||
payload = valid_config_payload.copy()
|
||||
payload["data"]["attributes"]["model"] = "invalid-model"
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert any("model" in error["source"]["pointer"] for error in errors)
|
||||
|
||||
def test_lighthouse_config_create_invalid_temperature_range(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
"""Test that temperature validation fails when out of range"""
|
||||
payload = valid_config_payload.copy()
|
||||
payload["data"]["attributes"]["temperature"] = 2.0 # Out of range
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert any("temperature" in error["source"]["pointer"] for error in errors)
|
||||
|
||||
def test_lighthouse_config_create_invalid_max_tokens(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
"""Test that max_tokens validation fails with invalid value"""
|
||||
payload = valid_config_payload.copy()
|
||||
payload["data"]["attributes"]["max_tokens"] = -1 # Invalid value
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
assert any("max_tokens" in error["source"]["pointer"] for error in errors)
|
||||
|
||||
def test_lighthouse_config_create_missing_required_fields(
|
||||
self, authenticated_client
|
||||
):
|
||||
"""Test that validation fails when required fields are missing"""
|
||||
payload = {"data": {"type": "lighthouse-configuration", "attributes": {}}}
|
||||
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
errors = response.json()["errors"]
|
||||
# Check for required fields
|
||||
required_fields = ["name", "api_key", "model"]
|
||||
for field in required_fields:
|
||||
assert any(field in error["source"]["pointer"] for error in errors)
|
||||
|
||||
def test_lighthouse_config_create_duplicate(
|
||||
self, authenticated_client, valid_config_payload
|
||||
):
|
||||
# Create first config
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=valid_config_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Try to create second config for same tenant
|
||||
response = authenticated_client.post(
|
||||
reverse("lighthouseconfiguration-list"),
|
||||
data=valid_config_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
"Lighthouse configuration already exists for this tenant"
|
||||
in response.json()["errors"][0]["detail"]
|
||||
)
|
||||
|
||||
def test_lighthouse_config_retrieve(
|
||||
self, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
"""Test retrieving a lighthouse config"""
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"lighthouseconfiguration-detail",
|
||||
kwargs={"pk": lighthouse_config_fixture.id},
|
||||
)
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data["attributes"]["name"] == lighthouse_config_fixture.name
|
||||
assert data["attributes"]["api_key"] == "*" * len(
|
||||
lighthouse_config_fixture.api_key
|
||||
)
|
||||
|
||||
def test_lighthouse_config_update(
|
||||
self, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
update_payload = {
|
||||
"data": {
|
||||
"type": "lighthouse-configuration",
|
||||
"id": str(lighthouse_config_fixture.id),
|
||||
"attributes": {
|
||||
"name": "Updated Config",
|
||||
"model": "gpt-4o-mini",
|
||||
"temperature": 0.5,
|
||||
},
|
||||
}
|
||||
}
|
||||
response = authenticated_client.patch(
|
||||
reverse(
|
||||
"lighthouseconfiguration-detail",
|
||||
kwargs={"pk": lighthouse_config_fixture.id},
|
||||
),
|
||||
data=update_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
assert data["attributes"]["name"] == "Updated Config"
|
||||
assert data["attributes"]["model"] == "gpt-4o-mini"
|
||||
assert data["attributes"]["temperature"] == 0.5
|
||||
|
||||
def test_lighthouse_config_delete(
|
||||
self, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
config_id = lighthouse_config_fixture.id
|
||||
response = authenticated_client.delete(
|
||||
reverse("lighthouseconfiguration-detail", kwargs={"pk": config_id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
# Verify deletion
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-detail", kwargs={"pk": config_id})
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@patch("api.v1.views.openai.OpenAI")
|
||||
def test_lighthouse_config_check_connection(
|
||||
self, mock_openai, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
config_id = lighthouse_config_fixture.id
|
||||
# Mock successful API call
|
||||
mock_client = Mock()
|
||||
mock_client.models.list.return_value = Mock(
|
||||
data=[Mock(id="gpt-4o"), Mock(id="gpt-4o-mini")]
|
||||
)
|
||||
mock_openai.return_value = mock_client
|
||||
# Check connection
|
||||
response = authenticated_client.get(
|
||||
reverse(
|
||||
"lighthouseconfiguration-check-connection", kwargs={"pk": config_id}
|
||||
)
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["detail"] == "Connection successful!"
|
||||
assert "gpt-4o" in response.json()["data"]["available_models"]
|
||||
assert "gpt-4o-mini" in response.json()["data"]["available_models"]
|
||||
|
||||
def test_lighthouse_config_get_key(
|
||||
self, authenticated_client, lighthouse_config_fixture, valid_config_payload
|
||||
):
|
||||
config_id = lighthouse_config_fixture.id
|
||||
expected_api_key = valid_config_payload["data"]["attributes"]["api_key"]
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-detail", kwargs={"pk": config_id})
|
||||
+ "?fields[lighthouse-config]=api_key"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["api_key"] == expected_api_key
|
||||
|
||||
def test_lighthouse_config_filters(
|
||||
self, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
# Test name filter
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-list")
|
||||
+ "?filter[name]="
|
||||
+ lighthouse_config_fixture.name
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
# Test model filter
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-list") + "?filter[model]=gpt-4o"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
# Test is_active filter
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-list") + "?filter[is_active]=true"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
|
||||
def test_lighthouse_config_sorting(
|
||||
self, authenticated_client, lighthouse_config_fixture
|
||||
):
|
||||
# Test sorting by name
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-list") + "?sort=name"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
# Test sorting by inserted_at
|
||||
response = authenticated_client.get(
|
||||
reverse("lighthouseconfiguration-list") + "?sort=-inserted_at"
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
@@ -21,7 +20,6 @@ from api.models import (
|
||||
IntegrationProviderRelationship,
|
||||
Invitation,
|
||||
InvitationRoleRelationship,
|
||||
LighthouseConfiguration,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
@@ -2130,137 +2128,3 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
|
||||
IntegrationProviderRelationship.objects.bulk_create(new_relationships)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class LighthouseConfigSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the LighthouseConfig model.
|
||||
"""
|
||||
|
||||
api_key = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"api_key",
|
||||
"model",
|
||||
"temperature",
|
||||
"max_tokens",
|
||||
"business_context",
|
||||
"is_active",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
# Check if api_key is specifically requested in fields param
|
||||
fields_param = self.context.get("request", None) and self.context[
|
||||
"request"
|
||||
].query_params.get("fields[lighthouse-config]", "")
|
||||
if fields_param == "api_key":
|
||||
# Return decrypted key if specifically requested
|
||||
data["api_key"] = instance.api_key_decoded if instance.api_key else None
|
||||
else:
|
||||
# Return masked key for general requests
|
||||
data["api_key"] = "*" * len(instance.api_key) if instance.api_key else None
|
||||
return data
|
||||
|
||||
|
||||
class LighthouseConfigCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
"""Serializer for creating new Lighthouse configurations."""
|
||||
|
||||
api_key = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"api_key",
|
||||
"model",
|
||||
"temperature",
|
||||
"max_tokens",
|
||||
"business_context",
|
||||
"is_active",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
tenant_id = self.context.get("request").tenant_id
|
||||
if LighthouseConfiguration.objects.filter(tenant_id=tenant_id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{
|
||||
"tenant_id": "Lighthouse configuration already exists for this tenant."
|
||||
}
|
||||
)
|
||||
return super().validate(attrs)
|
||||
|
||||
def create(self, validated_data):
|
||||
api_key = validated_data.pop("api_key")
|
||||
|
||||
# Validate API key format before creating the instance
|
||||
if not api_key:
|
||||
raise serializers.ValidationError({"api_key": "API key is required"})
|
||||
|
||||
# Validate OpenAI API key format
|
||||
openai_key_pattern = r"^sk-[\w-]+T3BlbkFJ[\w-]+$"
|
||||
if not re.match(openai_key_pattern, api_key):
|
||||
raise serializers.ValidationError(
|
||||
{"api_key": "Invalid OpenAI API key format."}
|
||||
)
|
||||
|
||||
instance = super().create(validated_data)
|
||||
instance.api_key_decoded = api_key
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class LighthouseConfigUpdateSerializer(BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for updating LighthouseConfig instances.
|
||||
"""
|
||||
|
||||
api_key = serializers.CharField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = LighthouseConfiguration
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"api_key",
|
||||
"model",
|
||||
"temperature",
|
||||
"max_tokens",
|
||||
"business_context",
|
||||
"is_active",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"name": {"required": False},
|
||||
"model": {"required": False},
|
||||
"temperature": {"required": False},
|
||||
"max_tokens": {"required": False},
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
api_key = validated_data.pop("api_key", None)
|
||||
instance = super().update(instance, validated_data)
|
||||
if api_key:
|
||||
instance.api_key_decoded = api_key
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@@ -13,7 +13,6 @@ from api.v1.views import (
|
||||
IntegrationViewSet,
|
||||
InvitationAcceptViewSet,
|
||||
InvitationViewSet,
|
||||
LighthouseConfigViewSet,
|
||||
MembershipViewSet,
|
||||
OverviewViewSet,
|
||||
ProviderGroupProvidersRelationshipView,
|
||||
@@ -50,11 +49,6 @@ router.register(
|
||||
router.register(r"overviews", OverviewViewSet, basename="overview")
|
||||
router.register(r"schedules", ScheduleViewSet, basename="schedule")
|
||||
router.register(r"integrations", IntegrationViewSet, basename="integration")
|
||||
router.register(
|
||||
r"lighthouse-configuration",
|
||||
LighthouseConfigViewSet,
|
||||
basename="lighthouseconfiguration",
|
||||
)
|
||||
|
||||
tenants_router = routers.NestedSimpleRouter(router, r"tenants", lookup="tenant")
|
||||
tenants_router.register(
|
||||
|
||||
@@ -2,7 +2,6 @@ import glob
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import openai
|
||||
import sentry_sdk
|
||||
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
|
||||
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
|
||||
@@ -85,7 +84,6 @@ from api.models import (
|
||||
Finding,
|
||||
Integration,
|
||||
Invitation,
|
||||
LighthouseConfiguration,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
@@ -128,9 +126,6 @@ from api.v1.serializers import (
|
||||
InvitationCreateSerializer,
|
||||
InvitationSerializer,
|
||||
InvitationUpdateSerializer,
|
||||
LighthouseConfigCreateSerializer,
|
||||
LighthouseConfigSerializer,
|
||||
LighthouseConfigUpdateSerializer,
|
||||
MembershipSerializer,
|
||||
OverviewFindingSerializer,
|
||||
OverviewProviderSerializer,
|
||||
@@ -1091,7 +1086,7 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
task = check_provider_connection_task.delay(
|
||||
provider_id=pk, tenant_id=self.request.tenant_id
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
prowler_task = Task.objects.get_with_retry(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
@@ -1114,7 +1109,7 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
task = delete_provider_task.delay(
|
||||
provider_id=pk, tenant_id=self.request.tenant_id
|
||||
)
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
prowler_task = Task.objects.get_with_retry(id=task.id)
|
||||
serializer = TaskSerializer(prowler_task)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
@@ -1494,10 +1489,10 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
},
|
||||
)
|
||||
|
||||
prowler_task = Task.objects.get_with_retry(id=task.id)
|
||||
scan.task_id = task.id
|
||||
scan.save(update_fields=["task_id"])
|
||||
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
self.response_serializer_class = TaskSerializer
|
||||
output_serializer = self.get_serializer(prowler_task)
|
||||
|
||||
@@ -2828,7 +2823,7 @@ class ScheduleViewSet(BaseRLSViewSet):
|
||||
with transaction.atomic():
|
||||
task = schedule_provider_scan(provider_instance)
|
||||
|
||||
prowler_task = Task.objects.get(id=task.id)
|
||||
prowler_task = Task.objects.get_with_retry(id=task.id)
|
||||
self.response_serializer_class = TaskSerializer
|
||||
output_serializer = self.get_serializer(prowler_task)
|
||||
|
||||
@@ -2905,105 +2900,3 @@ class IntegrationViewSet(BaseRLSViewSet):
|
||||
context = super().get_serializer_context()
|
||||
context["allowed_providers"] = self.allowed_providers
|
||||
return context
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="List all Lighthouse configurations",
|
||||
description="Retrieve a list of all Lighthouse configurations.",
|
||||
),
|
||||
create=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Create a new Lighthouse configuration",
|
||||
description="Create a new Lighthouse configuration with the specified details.",
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Partially update a Lighthouse configuration",
|
||||
description="Update certain fields of an existing Lighthouse configuration.",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Retrieve a Lighthouse configuration",
|
||||
description="Fetch detailed information about a specific Lighthouse configuration by its ID. Add query param `fields[lighthouse-config]=api_key` to get API key.",
|
||||
),
|
||||
destroy=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Delete a Lighthouse configuration",
|
||||
description="Remove a Lighthouse configuration by its ID.",
|
||||
),
|
||||
check_connection=extend_schema(
|
||||
tags=["Lighthouse"],
|
||||
summary="Check the connection to the OpenAI API",
|
||||
description="Verify the connection to the OpenAI API for a specific Lighthouse configuration.",
|
||||
),
|
||||
)
|
||||
class LighthouseConfigViewSet(BaseRLSViewSet):
|
||||
"""
|
||||
API endpoint for managing Lighthouse configuration.
|
||||
"""
|
||||
|
||||
filterset_fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
"model": ["exact", "icontains"],
|
||||
"is_active": ["exact"],
|
||||
"inserted_at": ["gte", "lte"],
|
||||
}
|
||||
ordering_fields = ["name", "inserted_at", "updated_at", "is_active"]
|
||||
ordering = ["-inserted_at"]
|
||||
|
||||
def get_queryset(self):
|
||||
return LighthouseConfiguration.objects.filter(tenant_id=self.request.tenant_id)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return LighthouseConfigCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return LighthouseConfigUpdateSerializer
|
||||
return LighthouseConfigSerializer
|
||||
|
||||
@action(detail=True, methods=["get"])
|
||||
def check_connection(self, request, pk=None):
|
||||
"""
|
||||
Check the connection to the OpenAI API.
|
||||
"""
|
||||
instance = self.get_object()
|
||||
if not instance.api_key_decoded:
|
||||
return Response(
|
||||
{"detail": "API key is invalid or missing."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
client = openai.OpenAI(
|
||||
api_key=instance.api_key_decoded,
|
||||
)
|
||||
models = client.models.list()
|
||||
return Response(
|
||||
{
|
||||
"detail": "Connection successful!",
|
||||
"available_models": [model.id for model in models.data],
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{"detail": f"Connection failed: {str(e)}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""Create new Lighthouse configuration"""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = serializer.save()
|
||||
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
LighthouseConfigSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ from api.models import (
|
||||
Integration,
|
||||
IntegrationProviderRelationship,
|
||||
Invitation,
|
||||
LighthouseConfiguration,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
@@ -930,20 +929,6 @@ def backfill_scan_metadata_fixture(scans_fixture, findings_fixture):
|
||||
backfill_resource_scan_summaries(tenant_id=tenant_id, scan_id=scan_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lighthouse_config_fixture(authenticated_client, tenants_fixture):
|
||||
return LighthouseConfiguration.objects.create(
|
||||
tenant_id=tenants_fixture[0].id,
|
||||
name="OpenAI",
|
||||
api_key_decoded="sk-test1234567890T3BlbkFJtest1234567890",
|
||||
model="gpt-4o",
|
||||
temperature=0,
|
||||
max_tokens=4000,
|
||||
business_context="Test business context",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def latest_scan_finding(authenticated_client, providers_fixture, resources_fixture):
|
||||
provider = providers_fixture[0]
|
||||
|
||||
@@ -124,6 +124,7 @@ def perform_prowler_scan(
|
||||
unique_resources = set()
|
||||
scan_resource_cache: set[tuple[str, str, str, str]] = set()
|
||||
start_time = time.time()
|
||||
exc = None
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
provider_instance = Provider.objects.get(pk=provider_id)
|
||||
@@ -139,7 +140,7 @@ def perform_prowler_scan(
|
||||
provider_instance.connected = True
|
||||
except Exception as e:
|
||||
provider_instance.connected = False
|
||||
raise ValueError(
|
||||
exc = ValueError(
|
||||
f"Provider {provider_instance.provider} is not connected: {e}"
|
||||
)
|
||||
finally:
|
||||
@@ -148,6 +149,11 @@ def perform_prowler_scan(
|
||||
)
|
||||
provider_instance.save()
|
||||
|
||||
# If the provider is not connected, raise an exception outside the transaction.
|
||||
# If raised within the transaction, the transaction will be rolled back and the provider will not be marked as not connected.
|
||||
if exc:
|
||||
raise exc
|
||||
|
||||
prowler_scan = ProwlerScan(provider=prowler_provider, checks=checks_to_execute)
|
||||
|
||||
resource_cache = {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -206,6 +207,10 @@ class TestPerformScan:
|
||||
scan.refresh_from_db()
|
||||
assert scan.state == StateChoices.FAILED
|
||||
|
||||
provider.refresh_from_db()
|
||||
assert provider.connected is False
|
||||
assert isinstance(provider.connection_last_checked_at, datetime)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"last_status, new_status, expected_delta",
|
||||
[
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import warnings
|
||||
|
||||
from dashboard.common_methods import get_section_containers_3_levels
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def get_table(data):
|
||||
data["REQUIREMENTS_DESCRIPTION"] = (
|
||||
data["REQUIREMENTS_ID"] + " - " + data["REQUIREMENTS_DESCRIPTION"]
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_DESCRIPTION"] = data["REQUIREMENTS_DESCRIPTION"].apply(
|
||||
lambda x: x[:150] + "..." if len(str(x)) > 150 else x
|
||||
)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION"
|
||||
].apply(lambda x: x[:80] + "..." if len(str(x)) > 80 else x)
|
||||
|
||||
data["REQUIREMENTS_ATTRIBUTES_SUBSECTION"] = data[
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION"
|
||||
].apply(lambda x: x[:150] + "..." if len(str(x)) > 150 else x)
|
||||
|
||||
aux = data[
|
||||
[
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"CHECKID",
|
||||
"STATUS",
|
||||
"REGION",
|
||||
"ACCOUNTID",
|
||||
"RESOURCEID",
|
||||
]
|
||||
]
|
||||
|
||||
return get_section_containers_3_levels(
|
||||
aux,
|
||||
"REQUIREMENTS_ATTRIBUTES_SECTION",
|
||||
"REQUIREMENTS_ATTRIBUTES_SUBSECTION",
|
||||
"REQUIREMENTS_DESCRIPTION",
|
||||
)
|
||||
@@ -90,12 +90,28 @@ def create_layout_overview(
|
||||
),
|
||||
html.Div(
|
||||
[
|
||||
(
|
||||
html.Label(
|
||||
"Table Rows:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
)
|
||||
html.Label(
|
||||
"Search:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
),
|
||||
dcc.Input(
|
||||
id="search-input",
|
||||
type="text",
|
||||
placeholder="Search by check title, service, region...",
|
||||
debounce=True,
|
||||
style={
|
||||
"padding": "4px 8px",
|
||||
"border": "1px solid #ccc",
|
||||
"borderRadius": "4px",
|
||||
"marginRight": "20px",
|
||||
"width": "250px",
|
||||
},
|
||||
),
|
||||
html.Label(
|
||||
"Table Rows:",
|
||||
className="text-prowler-stone-900 font-bold text-sm",
|
||||
style={"margin-right": "10px"},
|
||||
),
|
||||
table_row_dropdown,
|
||||
download_button_csv,
|
||||
|
||||
@@ -518,6 +518,7 @@ else:
|
||||
Input("service-filter", "value"),
|
||||
Input("table-rows", "value"),
|
||||
Input("status-filter", "value"),
|
||||
Input("search-input", "value"),
|
||||
Input("aws_card", "n_clicks"),
|
||||
Input("azure_card", "n_clicks"),
|
||||
Input("gcp_card", "n_clicks"),
|
||||
@@ -540,6 +541,7 @@ def filter_data(
|
||||
service_values,
|
||||
table_row_values,
|
||||
status_values,
|
||||
search_value,
|
||||
aws_clicks,
|
||||
azure_clicks,
|
||||
gcp_clicks,
|
||||
@@ -1144,6 +1146,15 @@ def filter_data(
|
||||
}
|
||||
|
||||
index_count = 0
|
||||
if search_value:
|
||||
search_value = search_value.lower()
|
||||
filtered_data = filtered_data[
|
||||
filtered_data["CHECK_TITLE"].str.lower().str.contains(search_value)
|
||||
| filtered_data["SERVICE_NAME"].str.lower().str.contains(search_value)
|
||||
| filtered_data["REGION"].str.lower().str.contains(search_value)
|
||||
| filtered_data["STATUS"].str.lower().str.contains(search_value)
|
||||
]
|
||||
|
||||
full_filtered_data = filtered_data.copy()
|
||||
filtered_data = filtered_data.head(table_row_values)
|
||||
# Sort the filtered_data
|
||||
|
||||
@@ -70,7 +70,7 @@ The other three cases does not need additional configuration, `--az-cli-auth` an
|
||||
Prowler for Azure needs two types of permission scopes to be set:
|
||||
|
||||
- **Microsoft Entra ID permissions**: used to retrieve metadata from the identity assumed by Prowler and specific Entra checks (not mandatory to have access to execute the tool). The permissions required by the tool are the following:
|
||||
- `Directory.Read.All`
|
||||
- `Domain.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
|
||||
- **Subscription scope permissions**: required to launch the checks against your resources, mandatory to launch the tool. It is required to add the following RBAC builtin roles per subscription to the entity that is going to be assumed by the tool:
|
||||
@@ -163,70 +163,24 @@ export AZURE_CLIENT_ID="XXXXXXXXX"
|
||||
export AZURE_CLIENT_SECRET="XXXXXXXXX"
|
||||
export AZURE_TENANT_ID="XXXXXXXXX"
|
||||
export M365_USER="your_email@example.com"
|
||||
export M365_PASSWORD="6500780061006d0070006c006500700061007300730077006f0072006400" # replace this to yours
|
||||
export M365_PASSWORD="examplepassword"
|
||||
```
|
||||
|
||||
These two new environment variables are **required** to execute the PowerShell modules needed to retrieve information from M365 services. Prowler uses Service Principal authentication to access Microsoft Graph and user credentials to authenticate to Microsoft PowerShell modules.
|
||||
|
||||
- `M365_USER` should be your Microsoft account email using the default domain. This means it must look like `example@YourCompany.onmicrosoft.com`.
|
||||
- `M365_USER` should be your Microsoft account email using the **assigned domain in the tenant**. This means it must look like `example@YourCompany.onmicrosoft.com` or `example@YourCompany.com`, but it must be the exact domain assigned to that user in the tenant.
|
||||
|
||||
To ensure that you are using the default domain you can see how to verify it [here](../tutorials/microsoft365/getting-started-m365.md#step-1-obtain-your-domain).
|
||||
???+ warning
|
||||
Using a tenant domain other than the one assigned — even if it belongs to the same tenant — will cause Prowler to fail, as Microsoft authentication will not succeed.
|
||||
|
||||
If you don't have a user created with that domain, Prowler will not work as it will not be able to ensure both app an user belong to the same tenant. To proceed, you can either create a new user with that domain or modify the domain of an existing user.
|
||||
Ensure you are using the right domain for the user you are trying to authenticate with.
|
||||
|
||||

|
||||
|
||||
- `M365_PASSWORD` must be an encrypted SecureString. To convert your password into a valid encrypted string, you need to use PowerShell.
|
||||
|
||||
???+ warning
|
||||
Passwords encrypted using ConvertTo-SecureString can only be decrypted on the same OS/user context. If you generate an encrypted password on macOS or Linux (both UNIX), it should fail on Windows and vice versa. As Prowler Cloud runs on UNIX if you generate your password using Windows it won't work so you'll need to generate a new password using any UNIX distro (example above)
|
||||
|
||||
If you are working from Windows and you will use your encrypted password in a different system (like for example executing Prowler in macOS or adding your password to Prowler Cloud), you will need to generate a "UNIX compatible" version of your encrypted password. This can be done using WSL which is so easy to install on Windows.
|
||||
|
||||
=== "UNIX"
|
||||
|
||||
Open a PowerShell cmd with a [supported version](requirements.md#supported-powershell-versions) and then run the following command:
|
||||
|
||||
```console
|
||||
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
|
||||
$encryptedPassword = $securePassword | ConvertFrom-SecureString
|
||||
Write-Output $encryptedPassword
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
|
||||
=== "Windows"
|
||||
|
||||
|
||||
How to install WSL and PowerShell on it to generate that password (you can use a different distro but this one will work for sure):
|
||||
|
||||
```console
|
||||
wsl --install -d Ubuntu-22.04
|
||||
```
|
||||
|
||||
Then, open the Ubuntu terminal and run the following commands:
|
||||
|
||||
```console
|
||||
sudo apt update && sudo apt install -y wget apt-transport-https software-properties-common
|
||||
wget -q "https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb"
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
sudo apt update
|
||||
sudo apt install -y powershell
|
||||
pwsh
|
||||
```
|
||||
|
||||
With this done you will see now that a prompt running PowerShell with the latest version is open so here you will be able to generate your encrypted password:
|
||||
|
||||
```console
|
||||
$securePassword = ConvertTo-SecureString "examplepassword" -AsPlainText -Force
|
||||
$encryptedPassword = $securePassword | ConvertFrom-SecureString
|
||||
Write-Output $encryptedPassword
|
||||
6500780061006d0070006c006500700061007300730077006f0072006400
|
||||
```
|
||||
|
||||
If everything is done correctly, you will see the encrypted string that you need to set as the `M365_PASSWORD` environment variable.
|
||||
- `M365_PASSWORD` must be the user password.
|
||||
|
||||
???+ note
|
||||
Before we asked for a encrypted password, but now we ask for the user password directly. Prowler will now handle the password encryption for you.
|
||||
|
||||
|
||||
### Interactive Browser authentication
|
||||
@@ -245,10 +199,9 @@ Since this is a delegated permission authentication method, necessary permission
|
||||
Prowler for M365 requires two types of permission scopes to be set (if you want to run the full provider including PowerShell checks). Both must be configured using Microsoft Entra ID:
|
||||
|
||||
- **Service Principal Application Permissions**: These are set at the **application** level and are used to retrieve data from the identity being assessed:
|
||||
- `Directory.Read.All`: Required for all services.
|
||||
- `Domain.Read.All`: Required for all services.
|
||||
- `Policy.Read.All`: Required for all services.
|
||||
- `User.Read` (IMPORTANT: this must be set as **delegated**): Required for the sign-in.
|
||||
- `Sites.Read.All`: Required for SharePoint service.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 119 KiB |
@@ -40,7 +40,7 @@ az ad sp create-for-rbac --name "ProwlerApp"
|
||||
|
||||
To allow Prowler to retrieve metadata from the identity assumed and run specific Entra checks, it is needed to assign the following permissions:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Domain.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All` (used only for the Entra checks related with multifactor authentication)
|
||||
|
||||
@@ -58,7 +58,7 @@ To assign the permissions you can make it from the Azure Portal or using the Azu
|
||||
5. Then click on "+ Add a permission" and select "Microsoft Graph"
|
||||
6. Once in the "Microsoft Graph" view, select "Application permissions"
|
||||
7. Finally, search for "Directory", "Policy" and "UserAuthenticationMethod" select the following permissions:
|
||||
- `Directory.Read.All`
|
||||
- `Domain.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All`
|
||||
8. Click on "Add permissions" to apply the new permissions.
|
||||
|
||||
@@ -90,7 +90,7 @@ A Service Principal is required to grant Prowler the necessary privileges.
|
||||
|
||||
Assign the following Microsoft Graph permissions:
|
||||
|
||||
- Directory.Read.All
|
||||
- Domain.Read.All
|
||||
|
||||
- Policy.Read.All
|
||||
|
||||
@@ -107,11 +107,11 @@ Assign the following Microsoft Graph permissions:
|
||||
|
||||
3. Search and select:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `Domain.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `UserAuthenticationMethod.Read.All`
|
||||
|
||||

|
||||

|
||||
|
||||
4. Click `Add permissions`, then grant admin consent
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 160 KiB |
@@ -4,9 +4,9 @@ Set up your M365 account to enable security scanning using Prowler Cloud/App.
|
||||
|
||||
## Requirements
|
||||
|
||||
To configure your M365 account, you’ll need:
|
||||
To configure your M365 account, you'll need:
|
||||
|
||||
1. Obtain your `Default Domain` from the Entra ID portal.
|
||||
1. Obtain a domain from the Entra ID portal.
|
||||
|
||||
2. Access Prowler Cloud/App and add a new cloud provider `Microsoft 365`.
|
||||
|
||||
@@ -18,8 +18,6 @@ To configure your M365 account, you’ll need:
|
||||
|
||||
3.3 Assign the required roles to your user.
|
||||
|
||||
3.4 Retrieve your encrypted password.
|
||||
|
||||
4. Add the credentials to Prowler Cloud/App.
|
||||
|
||||
## Step 1: Obtain your Domain
|
||||
@@ -32,9 +30,7 @@ Go to the Entra ID portal, then you can search for `Domain` or go to Identity >
|
||||
|
||||

|
||||
|
||||
Once you are there just look for the `Default Domain` this should be something similar to `YourCompany.onmicrosoft.com`. To ensure that you are picking the correct domain just click on it and verify that the type is `Initial` and you can't delete it.
|
||||
|
||||

|
||||
Once you are there just select the domain you want to use.
|
||||
|
||||
---
|
||||
|
||||
@@ -78,11 +74,11 @@ A Service Principal is required to grant Prowler the necessary privileges.
|
||||
|
||||

|
||||
|
||||
4. Go to `Certificates & secrets` > `+ New client secret`
|
||||
4. Go to `Certificates & secrets` > `Client secrets` > `+ New client secret`
|
||||
|
||||

|
||||
|
||||
5. Fill in the required fields and click `Add`, then copy the generated value (that value will be `AZURE_CLIENT_SECRET`)
|
||||
5. Fill in the required fields and click `Add`, then copy the generated `value` (that value will be `AZURE_CLIENT_SECRET`)
|
||||
|
||||

|
||||
|
||||
@@ -99,12 +95,11 @@ With this done you will have all the needed keys, summarized in the following ta
|
||||
### Grant required API permissions
|
||||
|
||||
Assign the following Microsoft Graph permissions:
|
||||
|
||||
- `Directory.Read.All`: Required for all services.
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
- `Domain.Read.All`: Required for all services.
|
||||
- `Policy.Read.All`: Required for all services.
|
||||
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
|
||||
- `Sites.Read.All`: Required for SharePoint service.
|
||||
- `SharePointTenantSettings.Read.All`: Required for SharePoint service.
|
||||
- `User.Read` (IMPORTANT: this is set as **delegated**): Required for the sign-in.
|
||||
|
||||
Follow these steps to assign the permissions:
|
||||
|
||||
@@ -117,12 +112,12 @@ Follow these steps to assign the permissions:
|
||||

|
||||
|
||||
3. Search and select every permission below and once all are selected click on `Add permissions`:
|
||||
|
||||
- `Directory.Read.All`
|
||||
- `AuditLog.Read.All`: Required for Entra service.
|
||||
- `Domain.Read.All`
|
||||
- `Policy.Read.All`
|
||||
- `Sites.Read.All`
|
||||
- `SharePointTenantSettings.Read.All`
|
||||
|
||||
|
||||

|
||||
|
||||
4. Click `Add permissions`, then grant admin consent
|
||||
@@ -174,25 +169,20 @@ Follow these steps to assign the role:
|
||||
|
||||
---
|
||||
|
||||
### Get your encrypted password
|
||||
|
||||
For this step you will need to use PowerShell, here you will have to create your Encrypted Password based on the password of the User that you are going to use. For more information about how to generate this Password go [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended) and follow the steps needed to obtain `M365_PASSWORD`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Add credentials to Prowler Cloud/App
|
||||
|
||||
1. Go to your App Registration overview and copy the `Client ID` and `Tenant ID`
|
||||
|
||||

|
||||
|
||||
|
||||
2. Go to Prowler Cloud/App and paste:
|
||||
|
||||
- `Client ID`
|
||||
- `Tenant ID`
|
||||
- `AZURE_CLIENT_SECRET` from earlier
|
||||
- `M365_USER` your user using the default domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
|
||||
- `M365_PASSWORD` generated before
|
||||
- `M365_USER` the user using the correct assigned domain, more info [here](../../getting-started/requirements.md#service-principal-and-user-credentials-authentication-recommended)
|
||||
- `M365_PASSWORD` the password of the user
|
||||
|
||||

|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 459 KiB After Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 119 KiB |
@@ -4636,7 +4636,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"},
|
||||
@@ -4645,7 +4644,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"},
|
||||
@@ -4654,7 +4652,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"},
|
||||
@@ -4663,7 +4660,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"},
|
||||
@@ -4672,7 +4668,6 @@ files = [
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"},
|
||||
{file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"},
|
||||
{file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"},
|
||||
|
||||
@@ -20,11 +20,16 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Add `repository_secret_scanning_enabled` check for GitHub provider. [(#7759)](https://github.com/prowler-cloud/prowler/pull/7759)
|
||||
- Add `repository_default_branch_requires_codeowners_review` check for GitHub provider. [(#7753)](https://github.com/prowler-cloud/prowler/pull/7753)
|
||||
- Add NIS 2 compliance framework for AWS. [(7839)](https://github.com/prowler-cloud/prowler/pull/7839)
|
||||
- Add NIS 2 compliance framework for Azure. [(7857)](https://github.com/prowler-cloud/prowler/pull/7857)
|
||||
- Add search bar in Dashboard Overview page. [(#7804)](https://github.com/prowler-cloud/prowler/pull/7804)
|
||||
|
||||
### Fixed
|
||||
- Fix `m365_powershell test_credentials` to use sanitized credentials. [(#7761)](https://github.com/prowler-cloud/prowler/pull/7761)
|
||||
- Fix `admincenter_users_admins_reduced_license_footprint` check logic to pass when admin user has no license. [(#7779)](https://github.com/prowler-cloud/prowler/pull/7779)
|
||||
- Fix `m365_powershell` to close the PowerShell sessions in msgraph services. [(#7816)](https://github.com/prowler-cloud/prowler/pull/7816)
|
||||
- Fix `defender_ensure_notify_alerts_severity_is_high`check to accept high or lower severity. [(#7862)](https://github.com/prowler-cloud/prowler/pull/7862)
|
||||
- Replace `Directory.Read.All` permission with `Domain.Read.All` which is more restrictive. [(#7888)](https://github.com/prowler-cloud/prowler/pull/7888)
|
||||
- Split calls to list Azure Functions attributes. [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
|
||||
|
||||
---
|
||||
|
||||
@@ -54,6 +59,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
- Cover policies with conditions with SNS endpoint in `sns_topics_not_publicly_accessible`. [(#7750)](https://github.com/prowler-cloud/prowler/pull/7750)
|
||||
- Change severity logic for `ec2_securitygroup_allow_ingress_from_internet_to_all_ports` check. [(#7764)](https://github.com/prowler-cloud/prowler/pull/7764)
|
||||
- Automatically encrypt password in Microsoft365 provider. [(#7784)](https://github.com/prowler-cloud/prowler/pull/7784)
|
||||
- Split calls to list Azure Functions attributes. [(#7778)](https://github.com/prowler-cloud/prowler/pull/7778)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -329,9 +329,8 @@ class CheckMetadata(BaseModel):
|
||||
checks = set()
|
||||
|
||||
if service:
|
||||
# This is a special case for the AWS provider since `lambda` is a reserved keyword in Python
|
||||
if service == "awslambda":
|
||||
service = "lambda"
|
||||
if service == "lambda":
|
||||
service = "awslambda"
|
||||
checks = {
|
||||
check_name
|
||||
for check_name, check_metadata in bulk_checks_metadata.items()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"Provider": "azure",
|
||||
"CheckID": "defender_ensure_notify_alerts_severity_is_high",
|
||||
"CheckTitle": "Ensure That 'Notify about alerts with the following severity' is Set to 'High'",
|
||||
"CheckTitle": "Ensure that email notifications are configured for alerts with a minimum severity of 'High' or lower",
|
||||
"CheckType": [],
|
||||
"ServiceName": "defender",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "high",
|
||||
"ResourceType": "AzureEmailNotifications",
|
||||
"Description": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.",
|
||||
"Risk": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.",
|
||||
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details",
|
||||
"Description": "Microsoft Defender for Cloud sends email notifications when alerts of a certain severity level or higher are triggered. By setting the minimum severity to 'High', 'Medium', or even 'Low', you ensure that alerts with equal or greater severity (e.g., High or Critical) are still delivered. Selecting a lower threshold like 'Low' results in more comprehensive alert coverage.",
|
||||
"Risk": "If this setting is too restrictive (e.g., set to 'Critical' only), important security alerts with 'High' or 'Medium' severity might be missed. Ensuring that 'High' or a lower threshold is configured helps security teams stay informed about significant threats and respond in a timely manner.",
|
||||
"RelatedUrl": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/email-notifications-alerts#manage-notifications-on-email",
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Terraform": "https://docs.prowler.com/checks/azure/azure-general-policies/bc_azr_general_4#terraform"
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "1. From Azure Home select the Portal Menu 2. Select Microsoft Defender for Cloud 3. Click on Environment Settings 4. Click on the appropriate Management Group, Subscription, or Workspace 5. Click on Email notifications 6. Enter a valid security contact email address (or multiple addresses separated by commas) in the Additional email addresses field 7. Click Save",
|
||||
"Text": "1. From Azure Home select the Portal Menu. 2. Select Microsoft Defender for Cloud. 3. Click on Environment Settings. 4. Click on the appropriate Management Group, Subscription, or Workspace. 5. Click on Email notifications. 6. Under 'Notify about alerts with the following severity (or higher)', select at least 'High' (or optionally 'Medium' or 'Low' for broader coverage). 7. Click Save.",
|
||||
"Url": "https://docs.microsoft.com/en-us/rest/api/securitycenter/securitycontacts/list"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,12 +13,15 @@ class defender_ensure_notify_alerts_severity_is_high(Check):
|
||||
for contact in security_contacts.values():
|
||||
report = Check_Report_Azure(metadata=self.metadata(), resource=contact)
|
||||
report.subscription = subscription_name
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Notifiy alerts are enabled for severity high in subscription {subscription_name}."
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}."
|
||||
|
||||
if contact.alert_notifications_minimal_severity != "High":
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Notifiy alerts are not enabled for severity high in subscription {subscription_name}."
|
||||
if (
|
||||
contact.alert_notifications_minimal_severity != "Critical"
|
||||
and contact.alert_notifications_minimal_severity != ""
|
||||
):
|
||||
report.status = "PASS"
|
||||
report.status_extended = f"Notifications are enabled for alerts with a minimum severity of high or lower ({contact.alert_notifications_minimal_severity}) in subscription {subscription_name}."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
|
||||
@@ -515,9 +515,9 @@ class GcpProvider(Provider):
|
||||
credentials=session,
|
||||
)
|
||||
|
||||
# Test the connection using the Service Usage API since it is enabled by default
|
||||
client = discovery.build("serviceusage", "v1", credentials=session)
|
||||
request = client.services().list(parent=f"projects/{project_id}")
|
||||
# Test the connection using OAuth2 API to verify token validity
|
||||
client = discovery.build("oauth2", "v2", credentials=session)
|
||||
request = client.tokeninfo()
|
||||
request.execute()
|
||||
return Connection(is_connected=True)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class admincenter_groups_not_public_visibility(Check):
|
||||
report.status = "FAIL"
|
||||
report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private."
|
||||
|
||||
if group.visibility != "Public":
|
||||
if group.visibility and group.visibility != "Public":
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"Group {group.name} has {group.visibility} visibility."
|
||||
|
||||
@@ -11,8 +11,6 @@ from prowler.providers.m365.m365_provider import M365Provider
|
||||
class AdminCenter(M365Service):
|
||||
def __init__(self, provider: M365Provider):
|
||||
super().__init__(provider)
|
||||
if self.powershell:
|
||||
self.powershell.close()
|
||||
|
||||
self.organization_config = None
|
||||
self.sharing_policy = None
|
||||
@@ -203,7 +201,7 @@ class DirectoryRole(BaseModel):
|
||||
class Group(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
visibility: str
|
||||
visibility: Optional[str]
|
||||
|
||||
|
||||
class Domain(BaseModel):
|
||||
|
||||
@@ -493,6 +493,7 @@ class ConditionalAccessGrantControl(Enum):
|
||||
BLOCK = "block"
|
||||
DOMAIN_JOINED_DEVICE = "domainJoinedDevice"
|
||||
PASSWORD_CHANGE = "passwordChange"
|
||||
COMPLIANT_DEVICE = "compliantDevice"
|
||||
|
||||
|
||||
class GrantControlOperator(Enum):
|
||||
|
||||
@@ -37,7 +37,7 @@ mock_metadata_lambda = CheckMetadata(
|
||||
CheckID="awslambda_function_url_public",
|
||||
CheckTitle="Check 1",
|
||||
CheckType=["type1"],
|
||||
ServiceName="lambda",
|
||||
ServiceName="awslambda",
|
||||
SubServiceName="subservice1",
|
||||
ResourceIdTemplate="template1",
|
||||
Severity="high",
|
||||
|
||||
@@ -857,7 +857,6 @@ KISA_ISMSP_AWS = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_AWS_NAME = "prowler_threatscore_aws"
|
||||
PROWLER_THREATSCORE_AWS = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -901,7 +900,6 @@ PROWLER_THREATSCORE_AWS = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_AZURE_NAME = "prowler_threatscore_azure"
|
||||
PROWLER_THREATSCORE_AZURE = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -945,7 +943,6 @@ PROWLER_THREATSCORE_AZURE = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_GCP_NAME = "prowler_threatscore_gcp"
|
||||
PROWLER_THREATSCORE_GCP = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
@@ -989,7 +986,6 @@ PROWLER_THREATSCORE_GCP = Compliance(
|
||||
],
|
||||
)
|
||||
|
||||
PROWLER_THREATSCORE_M365_NAME = "prowler_threatscore_m365"
|
||||
PROWLER_THREATSCORE_M365 = Compliance(
|
||||
Framework="ProwlerThreatScore",
|
||||
Version="1.0",
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_aws import (
|
||||
ProwlerThreatScoreAWS,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_AWS,
|
||||
PROWLER_THREATSCORE_AWS_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AWS
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1
|
||||
|
||||
@@ -24,9 +21,7 @@ class TestProwlerThreatScoreAWS:
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreAWS(
|
||||
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreAWSModel)
|
||||
assert output_data.Provider == "aws"
|
||||
@@ -134,9 +129,7 @@ class TestProwlerThreatScoreAWS:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreAWS(
|
||||
findings, PROWLER_THREATSCORE_AWS, PROWLER_THREATSCORE_AWS_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAWS(findings, PROWLER_THREATSCORE_AWS)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_azure import (
|
||||
ProwlerThreatScoreAzure,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_AZURE,
|
||||
PROWLER_THREATSCORE_AZURE_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_AZURE
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
@@ -33,9 +30,7 @@ class TestProwlerThreatScoreAzure:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreAzure(
|
||||
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreAzureModel)
|
||||
assert output_data.Provider == "azure"
|
||||
@@ -144,9 +139,7 @@ class TestProwlerThreatScoreAzure:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreAzure(
|
||||
findings, PROWLER_THREATSCORE_AZURE, PROWLER_THREATSCORE_AZURE_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreAzure(findings, PROWLER_THREATSCORE_AZURE)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_gcp import (
|
||||
ProwlerThreatScoreGCP,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_GCP,
|
||||
PROWLER_THREATSCORE_GCP_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_GCP
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID
|
||||
|
||||
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreGCP:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreGCP(
|
||||
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreGCPModel)
|
||||
assert output_data.Provider == "gcp"
|
||||
@@ -140,9 +135,7 @@ class TestProwlerThreatScoreGCP:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreGCP(
|
||||
findings, PROWLER_THREATSCORE_GCP, PROWLER_THREATSCORE_GCP_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreGCP(findings, PROWLER_THREATSCORE_GCP)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -10,10 +10,7 @@ from prowler.lib.outputs.compliance.prowler_threatscore.models import (
|
||||
from prowler.lib.outputs.compliance.prowler_threatscore.prowler_threatscore_m365 import (
|
||||
ProwlerThreatScoreM365,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import (
|
||||
PROWLER_THREATSCORE_M365,
|
||||
PROWLER_THREATSCORE_M365_NAME,
|
||||
)
|
||||
from tests.lib.outputs.compliance.fixtures import PROWLER_THREATSCORE_M365
|
||||
from tests.lib.outputs.fixtures.fixtures import generate_finding_output
|
||||
from tests.providers.m365.m365_fixtures import TENANT_ID
|
||||
|
||||
@@ -30,9 +27,7 @@ class TestProwlerThreatScoreM365:
|
||||
)
|
||||
]
|
||||
|
||||
output = ProwlerThreatScoreM365(
|
||||
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
|
||||
output_data = output.data[0]
|
||||
assert isinstance(output_data, ProwlerThreatScoreM365Model)
|
||||
assert output_data.Provider == "m365"
|
||||
@@ -142,9 +137,7 @@ class TestProwlerThreatScoreM365:
|
||||
findings = [
|
||||
generate_finding_output(compliance={"ProwlerThreatScore-1.0": "1.1.1"})
|
||||
]
|
||||
output = ProwlerThreatScoreM365(
|
||||
findings, PROWLER_THREATSCORE_M365, PROWLER_THREATSCORE_M365_NAME
|
||||
)
|
||||
output = ProwlerThreatScoreM365(findings, PROWLER_THREATSCORE_M365)
|
||||
output._file_descriptor = mock_file
|
||||
|
||||
with patch.object(mock_file, "close", return_value=None):
|
||||
|
||||
@@ -31,7 +31,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
result = check.execute()
|
||||
assert len(result) == 0
|
||||
|
||||
def test_defender_severity_alerts_low(self):
|
||||
def test_defender_severity_alerts_critical(self):
|
||||
resource_id = str(uuid4())
|
||||
defender_client = mock.MagicMock
|
||||
defender_client.security_contacts = {
|
||||
@@ -41,7 +41,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
name="default",
|
||||
emails="",
|
||||
phone="",
|
||||
alert_notifications_minimal_severity="Low",
|
||||
alert_notifications_minimal_severity="Critical",
|
||||
alert_notifications_state="On",
|
||||
notified_roles=["Contributor"],
|
||||
notified_roles_state="On",
|
||||
@@ -69,7 +69,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
@@ -113,7 +113,51 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are enabled for alerts with a minimum severity of high or lower (High) in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
assert result[0].resource_id == resource_id
|
||||
|
||||
def test_defender_severity_alerts_low(self):
|
||||
resource_id = str(uuid4())
|
||||
defender_client = mock.MagicMock
|
||||
defender_client.security_contacts = {
|
||||
AZURE_SUBSCRIPTION_ID: {
|
||||
resource_id: SecurityContacts(
|
||||
resource_id=resource_id,
|
||||
name="default",
|
||||
emails="",
|
||||
phone="",
|
||||
alert_notifications_minimal_severity="Low",
|
||||
alert_notifications_state="On",
|
||||
notified_roles=["Contributor"],
|
||||
notified_roles_state="On",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_azure_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high.defender_client",
|
||||
new=defender_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.azure.services.defender.defender_ensure_notify_alerts_severity_is_high.defender_ensure_notify_alerts_severity_is_high import (
|
||||
defender_ensure_notify_alerts_severity_is_high,
|
||||
)
|
||||
|
||||
check = defender_ensure_notify_alerts_severity_is_high()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifications are enabled for alerts with a minimum severity of high or lower (Low) in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
@@ -156,7 +200,7 @@ class Test_defender_ensure_notify_alerts_severity_is_high:
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== f"Notifiy alerts are not enabled for severity high in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
== f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {AZURE_SUBSCRIPTION_ID}."
|
||||
)
|
||||
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
|
||||
assert result[0].resource_name == "default"
|
||||
|
||||
@@ -93,7 +93,7 @@ async def mock_entra_get_conditional_access_policy(_):
|
||||
"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"],
|
||||
"exclude": [],
|
||||
},
|
||||
access_controls={"grant": ["MFA"], "block": []},
|
||||
access_controls={"grant": ["MFA", "compliantDevice"], "block": []},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -216,7 +216,7 @@ class Test_Entra_Service:
|
||||
)
|
||||
assert entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
|
||||
"grant"
|
||||
] == ["MFA"]
|
||||
] == ["MFA", "compliantDevice"]
|
||||
assert (
|
||||
entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[
|
||||
"block"
|
||||
|
||||
@@ -114,3 +114,91 @@ class Test_admincenter_groups_not_public_visibility:
|
||||
assert result[0].resource_name == "Group1"
|
||||
assert result[0].resource_id == id_group1
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_admincenter_group_public_visibility(self):
|
||||
admincenter_client = mock.MagicMock
|
||||
admincenter_client.audited_tenant = "audited_tenant"
|
||||
admincenter_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client",
|
||||
new=admincenter_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import (
|
||||
admincenter_groups_not_public_visibility,
|
||||
)
|
||||
from prowler.providers.m365.services.admincenter.admincenter_service import (
|
||||
Group,
|
||||
)
|
||||
|
||||
id_group1 = str(uuid4())
|
||||
|
||||
admincenter_client.groups = {
|
||||
id_group1: Group(id=id_group1, name="Group1", visibility="Public"),
|
||||
}
|
||||
|
||||
check = admincenter_groups_not_public_visibility()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Group Group1 has Public visibility and should be Private."
|
||||
)
|
||||
assert result[0].resource == admincenter_client.groups[id_group1].dict()
|
||||
assert result[0].resource_name == "Group1"
|
||||
assert result[0].resource_id == id_group1
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_admincenter_group_none_visibility(self):
|
||||
admincenter_client = mock.MagicMock
|
||||
admincenter_client.audited_tenant = "audited_tenant"
|
||||
admincenter_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client",
|
||||
new=admincenter_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import (
|
||||
admincenter_groups_not_public_visibility,
|
||||
)
|
||||
from prowler.providers.m365.services.admincenter.admincenter_service import (
|
||||
Group,
|
||||
)
|
||||
|
||||
id_group1 = str(uuid4())
|
||||
|
||||
admincenter_client.groups = {
|
||||
id_group1: Group(id=id_group1, name="Group1", visibility=None),
|
||||
}
|
||||
|
||||
check = admincenter_groups_not_public_visibility()
|
||||
result = check.execute()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "Group Group1 has None visibility and should be Private."
|
||||
)
|
||||
assert result[0].resource == admincenter_client.groups[id_group1].dict()
|
||||
assert result[0].resource_name == "Group1"
|
||||
assert result[0].resource_id == id_group1
|
||||
assert result[0].location == "global"
|
||||
|
||||
@@ -68,7 +68,10 @@ async def mock_entra_get_conditional_access_policies(_):
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
|
||||
],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
@@ -211,7 +214,10 @@ class Test_Entra_Service:
|
||||
),
|
||||
),
|
||||
grant_controls=GrantControls(
|
||||
built_in_controls=[ConditionalAccessGrantControl.BLOCK],
|
||||
built_in_controls=[
|
||||
ConditionalAccessGrantControl.BLOCK,
|
||||
ConditionalAccessGrantControl.COMPLIANT_DEVICE,
|
||||
],
|
||||
operator=GrantControlOperator.OR,
|
||||
authentication_strength=AuthenticationStrength.PHISHING_RESISTANT_MFA,
|
||||
),
|
||||
|
||||
@@ -9,8 +9,22 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- New profile page with details about the user and their roles. [(#7780)](https://github.com/prowler-cloud/prowler/pull/7780)
|
||||
- Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- Add GCP credential method (Account Service Key). [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
|
||||
|
||||
---
|
||||
|
||||
## [v1.7.2] (Prowler v5.7.2)
|
||||
|
||||
### 🐞 Fixes
|
||||
|
||||
- Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758)
|
||||
- Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)]
|
||||
- Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865)
|
||||
- Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)]
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
parseStringify,
|
||||
wait,
|
||||
} from "@/lib";
|
||||
import { ProvidersApiResponse } from "@/types/providers";
|
||||
import { ProvidersApiResponse, ProviderType } from "@/types/providers";
|
||||
|
||||
export const getProviders = async ({
|
||||
page = 1,
|
||||
@@ -110,7 +110,7 @@ export const updateProvider = async (formData: FormData) => {
|
||||
export const addProvider = async (formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
|
||||
const providerType = formData.get("providerType") as string;
|
||||
const providerType = formData.get("providerType") as ProviderType;
|
||||
const providerUid = formData.get("providerUid") as string;
|
||||
const providerAlias = formData.get("providerAlias") as string;
|
||||
|
||||
@@ -152,9 +152,10 @@ export const addCredentialsProvider = async (formData: FormData) => {
|
||||
|
||||
const secretName = formData.get("secretName");
|
||||
const providerId = formData.get("providerId");
|
||||
const providerType = formData.get("providerType");
|
||||
const providerType = formData.get("providerType") as ProviderType;
|
||||
|
||||
const isRole = formData.get("role_arn") !== null;
|
||||
const isServiceAccount = formData.get("service_account_key") !== null;
|
||||
|
||||
let secret = {};
|
||||
let secretType = "static"; // Default to static credentials
|
||||
@@ -199,19 +200,36 @@ export const addCredentialsProvider = async (formData: FormData) => {
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
if (isServiceAccount) {
|
||||
// Service account configuration for GCP
|
||||
secretType = "service_account";
|
||||
const serviceAccountKeyRaw = formData.get(
|
||||
"service_account_key",
|
||||
) as string;
|
||||
|
||||
try {
|
||||
const serviceAccountKey = JSON.parse(serviceAccountKeyRaw);
|
||||
secret = {
|
||||
service_account_key: serviceAccountKey,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("error", error);
|
||||
}
|
||||
} else {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "kubernetes") {
|
||||
// Static credentials configuration for Kubernetes
|
||||
secret = {
|
||||
kubeconfig_content: formData.get("kubeconfig_content"),
|
||||
};
|
||||
}
|
||||
|
||||
const bodyData = {
|
||||
data: {
|
||||
type: "provider-secrets",
|
||||
@@ -257,9 +275,10 @@ export const updateCredentialsProvider = async (
|
||||
const url = new URL(`${apiBaseUrl}/providers/secrets/${credentialsId}`);
|
||||
|
||||
const secretName = formData.get("secretName");
|
||||
const providerType = formData.get("providerType");
|
||||
const providerType = formData.get("providerType") as ProviderType;
|
||||
|
||||
const isRole = formData.get("role_arn") !== null;
|
||||
const isServiceAccount = formData.get("service_account_key") !== null;
|
||||
|
||||
let secret = {};
|
||||
|
||||
@@ -302,12 +321,30 @@ export const updateCredentialsProvider = async (
|
||||
password: formData.get("password"),
|
||||
};
|
||||
} else if (providerType === "gcp") {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
if (isServiceAccount) {
|
||||
// Service account configuration for GCP
|
||||
const serviceAccountKeyRaw = formData.get(
|
||||
"service_account_key",
|
||||
) as string;
|
||||
|
||||
try {
|
||||
// Parse the service account key as JSON
|
||||
const serviceAccountKey = JSON.parse(serviceAccountKeyRaw);
|
||||
secret = {
|
||||
service_account_key: serviceAccountKey,
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("error", error);
|
||||
}
|
||||
} else {
|
||||
// Static credentials configuration for GCP
|
||||
secret = {
|
||||
client_id: formData.get("client_id"),
|
||||
client_secret: formData.get("client_secret"),
|
||||
refresh_token: formData.get("refresh_token"),
|
||||
};
|
||||
}
|
||||
} else if (providerType === "kubernetes") {
|
||||
// Static credentials configuration for Kubernetes
|
||||
secret = {
|
||||
|
||||
@@ -33,6 +33,7 @@ export default async function Compliance({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
},
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
if (!scansData?.data) {
|
||||
|
||||
@@ -4,10 +4,15 @@ import {
|
||||
ViaCredentialsForm,
|
||||
ViaRoleForm,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-via-aws/select-via-aws";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import {
|
||||
SelectViaGCP,
|
||||
ViaServiceAccountForm,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface Props {
|
||||
searchParams: { type: string; id: string; via?: string };
|
||||
searchParams: { type: ProviderType; id: string; via?: string };
|
||||
}
|
||||
|
||||
export default function AddCredentialsPage({ searchParams }: Props) {
|
||||
@@ -17,14 +22,24 @@ export default function AddCredentialsPage({ searchParams }: Props) {
|
||||
<SelectViaAWS initialVia={searchParams.via} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "gcp" && !searchParams.via && (
|
||||
<SelectViaGCP initialVia={searchParams.via} />
|
||||
)}
|
||||
|
||||
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
|
||||
searchParams.type !== "aws") && (
|
||||
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
|
||||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
|
||||
<ViaCredentialsForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "aws" && searchParams.via === "role" && (
|
||||
<ViaRoleForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "gcp" &&
|
||||
searchParams.via === "service-account" && (
|
||||
<ViaServiceAccountForm searchParams={searchParams} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { CredentialsUpdateInfo } from "@/components/providers";
|
||||
import {
|
||||
UpdateViaCredentialsForm,
|
||||
UpdateViaRoleForm,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-via-aws/select-via-aws";
|
||||
import { UpdateViaServiceAccountForm } from "@/components/providers/workflow/forms/update-via-service-account-key-form";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface Props {
|
||||
searchParams: { type: string; id: string; via?: string };
|
||||
searchParams: {
|
||||
type: ProviderType;
|
||||
id: string;
|
||||
via?: string;
|
||||
secretId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function UpdateCredentialsPage({ searchParams }: Props) {
|
||||
return (
|
||||
<>
|
||||
{searchParams.type === "aws" && !searchParams.via && (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-default-700">
|
||||
To update provider credentials,{" "}
|
||||
<strong>
|
||||
the same type that was originally configured must be used.
|
||||
</strong>
|
||||
</p>
|
||||
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-4 text-sm dark:text-default-300">
|
||||
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
|
||||
<p>
|
||||
If the provider was configured with static credentials, updates
|
||||
must also use static credentials. If it was configured with a
|
||||
role, updates must use a role.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-default-700">
|
||||
To switch from static credentials to a role (or vice versa), the
|
||||
provider must be deleted and set up again.
|
||||
</p>
|
||||
<SelectViaAWS initialVia={searchParams.via} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(searchParams.type === "aws" || searchParams.type === "gcp") &&
|
||||
!searchParams.via && (
|
||||
<CredentialsUpdateInfo
|
||||
providerType={searchParams.type}
|
||||
initialVia={searchParams.via}
|
||||
/>
|
||||
)}
|
||||
|
||||
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
|
||||
searchParams.type !== "aws") && (
|
||||
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
|
||||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
|
||||
<UpdateViaCredentialsForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "aws" && searchParams.via === "role" && (
|
||||
<UpdateViaRoleForm searchParams={searchParams} />
|
||||
)}
|
||||
|
||||
{searchParams.type === "gcp" &&
|
||||
searchParams.via === "service-account" && (
|
||||
<UpdateViaServiceAccountForm searchParams={searchParams} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ import GDPRLogo from "./gdpr.svg";
|
||||
import GxPLogo from "./gxp-aws.svg";
|
||||
import HIPAALogo from "./hipaa.svg";
|
||||
import ISOLogo from "./iso-27001.svg";
|
||||
import KISALogo from "./kisa.svg";
|
||||
import MITRELogo from "./mitre-attack.svg";
|
||||
import NIS2Logo from "./nis2.svg";
|
||||
import NISTLogo from "./nist.svg";
|
||||
import PCILogo from "./pci-dss.svg";
|
||||
import PROWLERTHREATLogo from "./prowlerThreat.svg";
|
||||
import RBILogo from "./rbi.svg";
|
||||
import SOC2Logo from "./soc2.svg";
|
||||
|
||||
@@ -60,4 +63,13 @@ export const getComplianceIcon = (complianceTitle: string) => {
|
||||
if (complianceTitle.toLowerCase().includes("soc2")) {
|
||||
return SOC2Logo;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("kisa")) {
|
||||
return KISALogo;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("prowlerthreatscore")) {
|
||||
return PROWLERTHREATLogo;
|
||||
}
|
||||
if (complianceTitle.toLowerCase().includes("nis2")) {
|
||||
return NIS2Logo;
|
||||
}
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M423 1492 l157 -157 0 -503 0 -502 175 0 175 0 0 250 c0 138 4 250 8
|
||||
250 4 0 30 -23 57 -51 l50 -51 160 4 c156 3 162 4 228 35 138 65 241 201 267
|
||||
350 38 230 -113 458 -344 519 -40 11 -163 14 -571 14 l-520 0 158 -158z m870
|
||||
-213 c29 -13 57 -58 57 -91 0 -40 -36 -86 -77 -98 -23 -7 -98 -10 -188 -8
|
||||
l-150 3 -3 103 -3 102 170 0 c101 0 180 -4 194 -11z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 790 B |
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface UpdateCredentialsInfoProps {
|
||||
providerType: ProviderType;
|
||||
initialVia?: string;
|
||||
}
|
||||
|
||||
export const CredentialsUpdateInfo = ({
|
||||
providerType,
|
||||
initialVia,
|
||||
}: UpdateCredentialsInfoProps) => {
|
||||
const renderSelectComponent = () => {
|
||||
if (providerType === "aws") {
|
||||
return <SelectViaAWS initialVia={initialVia} />;
|
||||
}
|
||||
if (providerType === "gcp") {
|
||||
return <SelectViaGCP initialVia={initialVia} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm text-default-700">
|
||||
To update provider credentials,{" "}
|
||||
<strong>
|
||||
the same type that was originally configured must be used.
|
||||
</strong>
|
||||
</p>
|
||||
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-4 text-sm dark:text-default-300">
|
||||
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
|
||||
<p>
|
||||
If the provider was configured with static credentials, updates must
|
||||
also use static credentials. If it was configured with a role in AWS
|
||||
(or service account in GCP),{" "}
|
||||
<strong>updates must use the same type.</strong>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-default-700">
|
||||
To switch from one type to another, the provider must be deleted and set
|
||||
up again.
|
||||
</p>
|
||||
{renderSelectComponent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./add-provider-button";
|
||||
export * from "./credentials-update-info";
|
||||
export * from "./forms/delete-form";
|
||||
export * from "./link-to-scans";
|
||||
export * from "./provider-info";
|
||||
|
||||
@@ -10,7 +10,6 @@ import * as z from "zod";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderType } from "@/types";
|
||||
|
||||
import { addProvider } from "../../../../actions/providers/providers";
|
||||
import { addProviderFormSchema, ApiError } from "../../../../types";
|
||||
@@ -171,7 +170,7 @@ export const ConnectAccountForm = () => {
|
||||
{/* Step 2: UID, alias, and credentials (if AWS) */}
|
||||
{prevStep === 2 && (
|
||||
<>
|
||||
<ProviderTitleDocs providerType={providerType as ProviderType} />
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="providerUid"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./connect-account-form";
|
||||
export * from "./radio-group-aws-via-credentials-form";
|
||||
export * from "./test-connection-form";
|
||||
export * from "./update-via-credentials-form";
|
||||
export * from "./update-via-role-form";
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Divider, Select, SelectItem, Spacer } from "@nextui-org/react";
|
||||
import { Control, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
|
||||
import { CredentialsRoleHelper } from "@/components/providers/workflow";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { AWSCredentialsRole } from "@/types";
|
||||
|
||||
import { CredentialsRoleHelper } from "../../credentials-role-helper";
|
||||
|
||||
export const AWSCredentialsRoleForm = ({
|
||||
export const AWSRoleCredentialsForm = ({
|
||||
control,
|
||||
setValue,
|
||||
externalId,
|
||||
@@ -3,7 +3,7 @@ import { Control } from "react-hook-form";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { AWSCredentials } from "@/types";
|
||||
|
||||
export const AWScredentialsForm = ({
|
||||
export const AWSStaticCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<AWSCredentials>;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./aws-role-credentials-form";
|
||||
export * from "./aws-static-credentials-form";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./radio-group-aws-via-credentials-type-form";
|
||||
export * from "./select-via-aws";
|
||||
@@ -14,7 +14,7 @@ type RadioGroupAWSViaCredentialsFormProps = {
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const RadioGroupAWSViaCredentialsForm = ({
|
||||
export const RadioGroupAWSViaCredentialsTypeForm = ({
|
||||
control,
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
import { RadioGroupAWSViaCredentialsForm } from "../radio-group-aws-via-credentials-form";
|
||||
import { RadioGroupAWSViaCredentialsTypeForm } from "./radio-group-aws-via-credentials-type-form";
|
||||
|
||||
interface SelectViaAWSProps {
|
||||
initialVia?: string;
|
||||
@@ -27,7 +27,7 @@ export const SelectViaAWS = ({ initialVia }: SelectViaAWSProps) => {
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<RadioGroupAWSViaCredentialsForm
|
||||
<RadioGroupAWSViaCredentialsTypeForm
|
||||
control={form.control}
|
||||
isInvalid={!!form.formState.errors.awsCredentialsType}
|
||||
errorMessage={form.formState.errors.awsCredentialsType?.message}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { GCPCredentials } from "@/types";
|
||||
import { GCPDefaultCredentials } from "@/types";
|
||||
|
||||
export const GCPcredentialsForm = ({
|
||||
export const GCPDefaultCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<GCPCredentials>;
|
||||
control: Control<GCPDefaultCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomTextarea } from "@/components/ui/custom";
|
||||
import { GCPServiceAccountKey } from "@/types";
|
||||
|
||||
export const GCPServiceAccountKeyForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<GCPServiceAccountKey>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md font-bold leading-9 text-default-foreground">
|
||||
Connect via Service Account Key
|
||||
</div>
|
||||
<div className="text-sm text-default-500">
|
||||
Please provide the service account key for your GCP credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomTextarea
|
||||
control={control}
|
||||
name="service_account_key"
|
||||
label="Service Account Key"
|
||||
labelPlacement="inside"
|
||||
placeholder="Paste your Service Account Key JSON content here"
|
||||
variant="bordered"
|
||||
minRows={10}
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.service_account_key}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./gcp-default-credentials-form";
|
||||
export * from "./gcp-service-account-key-form";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./radio-group-gcp-via-credentials-type-form";
|
||||
export * from "./select-via-gcp";
|
||||
export * from "./via-service-account-form";
|
||||
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { CustomRadio } from "@/components/ui/custom";
|
||||
import { FormMessage } from "@/components/ui/form";
|
||||
|
||||
type RadioGroupAWSViaCredentialsFormProps = {
|
||||
control: Control<any>;
|
||||
isInvalid: boolean;
|
||||
errorMessage?: string;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const RadioGroupGCPViaCredentialsTypeForm = ({
|
||||
control,
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
onChange,
|
||||
}: RadioGroupAWSViaCredentialsFormProps) => {
|
||||
return (
|
||||
<Controller
|
||||
name="gcpCredentialsType"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<RadioGroup
|
||||
className="flex flex-wrap"
|
||||
isInvalid={isInvalid}
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm text-default-500">
|
||||
Using Service Account
|
||||
</span>
|
||||
<CustomRadio
|
||||
description="Connect using Service Account"
|
||||
value="service-account"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">Connect via Service Account Key</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<span className="text-sm text-default-500">
|
||||
Using Application Default Credentials
|
||||
</span>
|
||||
<CustomRadio
|
||||
description="Connect via Credentials"
|
||||
value="credentials"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">
|
||||
Connect via Application Default Credentials
|
||||
</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-system-error dark:text-system-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
import { RadioGroupGCPViaCredentialsTypeForm } from "./radio-group-gcp-via-credentials-type-form";
|
||||
|
||||
interface SelectViaGCPProps {
|
||||
initialVia?: string;
|
||||
}
|
||||
|
||||
export const SelectViaGCP = ({ initialVia }: SelectViaGCPProps) => {
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
gcpCredentialsType: initialVia || "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSelectionChange = (value: string) => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("via", value);
|
||||
router.push(url.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<RadioGroupGCPViaCredentialsTypeForm
|
||||
control={form.control}
|
||||
isInvalid={!!form.formState.errors.gcpCredentialsType}
|
||||
errorMessage={form.formState.errors.gcpCredentialsType?.message}
|
||||
onChange={handleSelectionChange}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Control, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderTitleDocs } from "@/components/providers/workflow";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
addCredentialsServiceAccountFormSchema,
|
||||
ApiError,
|
||||
GCPServiceAccountKey,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
import { GCPServiceAccountKeyForm } from "./credentials-type/gcp-service-account-key-form";
|
||||
|
||||
export const ViaServiceAccountForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: ProviderType; id: string };
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const searchParamsObj = useSearchParams();
|
||||
|
||||
// Handler for back button
|
||||
const handleBackStep = () => {
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
currentParams.delete("via");
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const providerType = searchParams.type;
|
||||
const providerId = searchParams.id;
|
||||
|
||||
const formSchema = addCredentialsServiceAccountFormSchema(providerType);
|
||||
type FormSchemaType = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId,
|
||||
providerType,
|
||||
...(providerType === "gcp"
|
||||
? {
|
||||
service_account_key: "",
|
||||
secretName: "",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: FormSchemaType) => {
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== "") {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await addCredentialsProvider(formData);
|
||||
|
||||
if (data?.errors && data.errors.length > 0) {
|
||||
data.errors.forEach((error: ApiError) => {
|
||||
const errorMessage = error.detail;
|
||||
|
||||
switch (error.source.pointer) {
|
||||
case "/data/attributes/secret/service_account_key":
|
||||
form.setError("service_account_key" as keyof FormSchemaType, {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push(
|
||||
`/providers/test-connection?type=${providerType}&id=${providerId}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error during submission:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Submission failed",
|
||||
description: "An error occurred while processing your request.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "gcp" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
control={form.control as unknown as Control<GCPServiceAccountKey>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{searchParamsObj.get("via") === "service-account" && (
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Back"
|
||||
className="w-1/2 bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={handleBackStep}
|
||||
startContent={!isLoading && <ChevronLeftIcon size={24} />}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<span>Back</span>
|
||||
</CustomButton>
|
||||
)}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={"Save"}
|
||||
className="w-1/2"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Next</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./select-via-aws";
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
ApiError,
|
||||
AWSCredentials,
|
||||
AzureCredentials,
|
||||
GCPCredentials,
|
||||
GCPDefaultCredentials,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
} from "@/types";
|
||||
|
||||
import { ProviderTitleDocs } from "../provider-title-docs";
|
||||
import { AWScredentialsForm } from "./via-credentials/aws-credentials-form";
|
||||
import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { GCPcredentialsForm } from "./via-credentials/gcp-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
@@ -38,7 +38,7 @@ type FormType = CredentialsFormSchema &
|
||||
AWSCredentials &
|
||||
AzureCredentials &
|
||||
M365Credentials &
|
||||
GCPCredentials &
|
||||
GCPDefaultCredentials &
|
||||
KubernetesCredentials;
|
||||
|
||||
export const UpdateViaCredentialsForm = ({
|
||||
@@ -58,7 +58,7 @@ export const UpdateViaCredentialsForm = ({
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const providerType = searchParams.type;
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
const providerSecretId = searchParams.secretId || "";
|
||||
const formSchema = addCredentialsFormSchema(providerType);
|
||||
@@ -201,12 +201,12 @@ export const UpdateViaCredentialsForm = ({
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<ProviderTitleDocs providerType={providerType as ProviderType} />
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "aws" && (
|
||||
<AWScredentialsForm
|
||||
<AWSStaticCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentials>}
|
||||
/>
|
||||
)}
|
||||
@@ -221,8 +221,8 @@ export const UpdateViaCredentialsForm = ({
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" && (
|
||||
<GCPcredentialsForm
|
||||
control={form.control as unknown as Control<GCPCredentials>}
|
||||
<GCPDefaultCredentialsForm
|
||||
control={form.control as unknown as Control<GCPDefaultCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "kubernetes" && (
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
AWSCredentialsRole,
|
||||
} from "@/types";
|
||||
|
||||
import { AWSCredentialsRoleForm } from "./via-role/aws-role-form";
|
||||
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
|
||||
export const UpdateViaRoleForm = ({
|
||||
searchParams,
|
||||
@@ -150,7 +150,7 @@ export const UpdateViaRoleForm = ({
|
||||
|
||||
{/* Conditional AWS Form */}
|
||||
{providerType === "aws" && (
|
||||
<AWSCredentialsRoleForm
|
||||
<AWSRoleCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentialsRole>}
|
||||
setValue={
|
||||
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Divider } from "@nextui-org/react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Control, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { updateCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { ProviderTitleDocs } from "@/components/providers/workflow";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
addCredentialsServiceAccountFormSchema,
|
||||
ApiError,
|
||||
GCPServiceAccountKey,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
|
||||
export const UpdateViaServiceAccountForm = ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { type: string; id: string; secretId?: string };
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const searchParamsObj = useSearchParams();
|
||||
|
||||
// Handler for back button
|
||||
const handleBackStep = () => {
|
||||
const currentParams = new URLSearchParams(window.location.search);
|
||||
currentParams.delete("via");
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
const providerSecretId = searchParams.secretId || "";
|
||||
|
||||
const formSchema = addCredentialsServiceAccountFormSchema(providerType);
|
||||
type FormSchemaType = z.infer<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormSchemaType>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
providerId,
|
||||
providerType,
|
||||
...(providerType === "gcp"
|
||||
? {
|
||||
service_account_key: "",
|
||||
secretName: "",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = form.formState.isSubmitting;
|
||||
|
||||
const onSubmitClient = async (values: FormSchemaType) => {
|
||||
if (!providerSecretId) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Missing Secret ID",
|
||||
description: "Cannot update credentials without a valid secret ID.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== "") {
|
||||
formData.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await updateCredentialsProvider(providerSecretId, formData);
|
||||
if (data?.errors && data.errors.length > 0) {
|
||||
data.errors.forEach((error: ApiError) => {
|
||||
const errorMessage = error.detail;
|
||||
|
||||
switch (error.source.pointer) {
|
||||
case "/data/attributes/secret/service_account_key":
|
||||
form.setError("service_account_key" as keyof FormSchemaType, {
|
||||
type: "server",
|
||||
message: errorMessage,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.push(
|
||||
`/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error during submission:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Submission failed",
|
||||
description: "An error occurred while processing your request.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmitClient)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "gcp" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
control={form.control as unknown as Control<GCPServiceAccountKey>}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{searchParamsObj.get("via") === "service-account" && (
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Back"
|
||||
className="w-1/2 bg-transparent"
|
||||
variant="faded"
|
||||
size="lg"
|
||||
radius="lg"
|
||||
onPress={handleBackStep}
|
||||
startContent={!isLoading && <ChevronLeftIcon size={24} />}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
<span>Back</span>
|
||||
</CustomButton>
|
||||
)}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
ariaLabel={"Save"}
|
||||
className="w-1/2"
|
||||
variant="solid"
|
||||
color="action"
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>Next</span>}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -11,21 +11,21 @@ import { addCredentialsProvider } from "@/actions/providers/providers";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderType } from "@/types";
|
||||
import {
|
||||
addCredentialsFormSchema,
|
||||
ApiError,
|
||||
AWSCredentials,
|
||||
AzureCredentials,
|
||||
GCPCredentials,
|
||||
GCPDefaultCredentials,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
|
||||
import { ProviderTitleDocs } from "../provider-title-docs";
|
||||
import { AWScredentialsForm } from "./via-credentials/aws-credentials-form";
|
||||
import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
|
||||
import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { GCPcredentialsForm } from "./via-credentials/gcp-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
@@ -37,7 +37,7 @@ type CredentialsFormSchema = z.infer<
|
||||
type FormType = CredentialsFormSchema &
|
||||
AWSCredentials &
|
||||
AzureCredentials &
|
||||
GCPCredentials &
|
||||
GCPDefaultCredentials &
|
||||
KubernetesCredentials &
|
||||
M365Credentials;
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ViaCredentialsForm = ({
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const providerType = searchParams.type;
|
||||
const providerType = searchParams.type as ProviderType;
|
||||
const providerId = searchParams.id;
|
||||
const formSchema = addCredentialsFormSchema(providerType);
|
||||
|
||||
@@ -200,12 +200,12 @@ export const ViaCredentialsForm = ({
|
||||
<input type="hidden" name="providerId" value={providerId} />
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
<ProviderTitleDocs providerType={providerType as ProviderType} />
|
||||
<ProviderTitleDocs providerType={providerType} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "aws" && (
|
||||
<AWScredentialsForm
|
||||
<AWSStaticCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentials>}
|
||||
/>
|
||||
)}
|
||||
@@ -220,8 +220,8 @@ export const ViaCredentialsForm = ({
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" && (
|
||||
<GCPcredentialsForm
|
||||
control={form.control as unknown as Control<GCPCredentials>}
|
||||
<GCPDefaultCredentialsForm
|
||||
control={form.control as unknown as Control<GCPDefaultCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "kubernetes" && (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from "./aws-credentials-form";
|
||||
export * from "./azure-credentials-form";
|
||||
export * from "./gcp-credentials-form";
|
||||
export * from "./k8s-credentials-form";
|
||||
export * from "./m365-credentials-form";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
AWSCredentialsRole,
|
||||
} from "@/types";
|
||||
|
||||
import { AWSCredentialsRoleForm } from "./via-role/aws-role-form";
|
||||
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
|
||||
export const ViaRoleForm = ({
|
||||
searchParams,
|
||||
@@ -149,7 +149,7 @@ export const ViaRoleForm = ({
|
||||
<input type="hidden" name="providerType" value={providerType} />
|
||||
|
||||
{providerType === "aws" && (
|
||||
<AWSCredentialsRoleForm
|
||||
<AWSRoleCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentialsRole>}
|
||||
setValue={
|
||||
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./aws-role-form";
|
||||
@@ -10,167 +10,183 @@ import {
|
||||
PopoverTrigger,
|
||||
ScrollShadow,
|
||||
} from "@nextui-org/react";
|
||||
import { XCircle } from "lucide-react";
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { PlusCircleIcon } from "@/components/icons";
|
||||
import { useUrlFilters } from "@/hooks/use-url-filters";
|
||||
import { CustomDropdownFilterProps } from "@/types";
|
||||
|
||||
import { EntityInfoShort } from "../entities";
|
||||
|
||||
const filterSelectedClass =
|
||||
"inline-flex items-center border py-1 text-xs transition-colors border-transparent bg-default-500 text-secondary-foreground hover:bg-default-500/80 rounded-md px-2 font-normal";
|
||||
|
||||
export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
|
||||
export const CustomDropdownFilter = ({
|
||||
filter,
|
||||
onFilterChange,
|
||||
}) => {
|
||||
}: CustomDropdownFilterProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const { clearFilter } = useUrlFilters();
|
||||
const [groupSelected, setGroupSelected] = useState(new Set<string>());
|
||||
const [pendingClearFilter, setPendingClearFilter] = useState<string | null>(
|
||||
null,
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const filterValues = useMemo(() => filter?.values || [], [filter?.values]);
|
||||
const selectedValues = Array.from(groupSelected).filter(
|
||||
(value) => value !== "all",
|
||||
);
|
||||
const isAllSelected =
|
||||
selectedValues.length === filterValues.length && filterValues.length > 0;
|
||||
|
||||
const allFilterKeys = useMemo(() => filter?.values || [], [filter?.values]);
|
||||
|
||||
const getActiveFilter = useMemo(() => {
|
||||
const currentFilters: Record<string, string> = {};
|
||||
Array.from(searchParams.entries()).forEach(([key, value]) => {
|
||||
if (key.startsWith("filter[") && key.endsWith("]")) {
|
||||
const filterKey = key.slice(7, -1);
|
||||
if (filter && filter.key === filterKey) {
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
currentFilters[filterKey] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return currentFilters;
|
||||
}, [searchParams, filter]);
|
||||
|
||||
const memoizedFilterValues = useMemo(
|
||||
() => filter?.values || [],
|
||||
[filter?.values],
|
||||
);
|
||||
const activeFilterValue = useMemo(() => {
|
||||
const filterParam = searchParams.get(`filter[${filter?.key}]`);
|
||||
return filterParam ? filterParam.split(",") : [];
|
||||
}, [searchParams, filter?.key]);
|
||||
|
||||
// Sync URL state with component state
|
||||
useEffect(() => {
|
||||
if (filter && getActiveFilter[filter.key]) {
|
||||
const activeValues = getActiveFilter[filter.key].split(",");
|
||||
const newSelection = new Set(activeValues);
|
||||
if (newSelection.size === memoizedFilterValues.length) {
|
||||
if (activeFilterValue.length > 0) {
|
||||
const newSelection = new Set(activeFilterValue);
|
||||
if (newSelection.size === filterValues.length) {
|
||||
newSelection.add("all");
|
||||
}
|
||||
setGroupSelected(newSelection);
|
||||
} else {
|
||||
setGroupSelected(new Set());
|
||||
}
|
||||
}, [getActiveFilter, filter?.key, memoizedFilterValues, filter]);
|
||||
}, [activeFilterValue, filterValues.length]);
|
||||
|
||||
const updateSelection = useCallback(
|
||||
(newValues: string[]) => {
|
||||
const actualValues = newValues.filter((key) => key !== "all");
|
||||
const newSelection = new Set(actualValues);
|
||||
|
||||
// Auto-add "all" if all items are selected
|
||||
if (
|
||||
actualValues.length === filterValues.length &&
|
||||
filterValues.length > 0
|
||||
) {
|
||||
newSelection.add("all");
|
||||
}
|
||||
|
||||
setGroupSelected(newSelection);
|
||||
|
||||
// Notify parent with actual values (excluding "all")
|
||||
onFilterChange?.(filter.key, actualValues);
|
||||
},
|
||||
[filterValues.length, onFilterChange, filter.key],
|
||||
);
|
||||
|
||||
const onSelectionChange = useCallback(
|
||||
(keys: string[]) => {
|
||||
setGroupSelected((prevGroupSelected) => {
|
||||
const newSelection = new Set(keys);
|
||||
const currentSelection = Array.from(groupSelected);
|
||||
const newKeys = new Set(keys);
|
||||
const oldKeys = new Set(currentSelection);
|
||||
|
||||
if (
|
||||
newSelection.size === allFilterKeys.length &&
|
||||
!newSelection.has("all")
|
||||
) {
|
||||
return new Set(["all", ...allFilterKeys]);
|
||||
} else if (prevGroupSelected.has("all")) {
|
||||
newSelection.delete("all");
|
||||
return new Set(allFilterKeys.filter((key) => newSelection.has(key)));
|
||||
}
|
||||
return newSelection;
|
||||
});
|
||||
// Check if "all" was just toggled
|
||||
const allWasSelected = oldKeys.has("all");
|
||||
const allIsSelected = newKeys.has("all");
|
||||
|
||||
if (onFilterChange && filter) {
|
||||
const selectedValues = keys.filter((key) => key !== "all");
|
||||
onFilterChange(filter.key, selectedValues);
|
||||
if (allIsSelected && !allWasSelected) {
|
||||
// "all" was just selected - select all items
|
||||
updateSelection(filterValues);
|
||||
} else if (!allIsSelected && allWasSelected) {
|
||||
// "all" was just deselected - deselect all items
|
||||
updateSelection([]);
|
||||
} else if (allIsSelected && allWasSelected) {
|
||||
// "all" was already selected, but individual items changed
|
||||
// Remove "all" and keep only the individual selections
|
||||
const individualSelections = keys.filter((key) => key !== "all");
|
||||
updateSelection(individualSelections);
|
||||
} else {
|
||||
// Normal individual selection without "all"
|
||||
updateSelection(keys);
|
||||
}
|
||||
},
|
||||
[allFilterKeys, onFilterChange, filter],
|
||||
[groupSelected, updateSelection, filterValues],
|
||||
);
|
||||
|
||||
const handleSelectAllClick = useCallback(() => {
|
||||
setGroupSelected((prevGroupSelected: Set<string>) => {
|
||||
const newSelection: Set<string> = prevGroupSelected.has("all")
|
||||
? new Set()
|
||||
: new Set(["all", ...allFilterKeys]);
|
||||
const handleClearAll = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
updateSelection([]);
|
||||
},
|
||||
[updateSelection],
|
||||
);
|
||||
|
||||
if (onFilterChange && filter) {
|
||||
const selectedValues = Array.from(newSelection).filter(
|
||||
(key) => key !== "all",
|
||||
);
|
||||
onFilterChange(filter.key, selectedValues);
|
||||
}
|
||||
|
||||
return newSelection;
|
||||
});
|
||||
}, [allFilterKeys, onFilterChange, filter]);
|
||||
|
||||
// Update the pending clear filter
|
||||
const onClearFilter = useCallback((filterKey: string) => {
|
||||
setPendingClearFilter(filterKey);
|
||||
}, []);
|
||||
|
||||
// Execute the update in the router after the render
|
||||
useEffect(() => {
|
||||
if (pendingClearFilter && filter) {
|
||||
clearFilter(pendingClearFilter);
|
||||
setPendingClearFilter(null); // Reset the state
|
||||
}
|
||||
}, [pendingClearFilter, clearFilter, filter]);
|
||||
const getDisplayLabel = useCallback(
|
||||
(value: string) => {
|
||||
const entity = filter.valueLabelMapping?.find((entry) => entry[value])?.[
|
||||
value
|
||||
];
|
||||
return entity?.alias || entity?.uid || value;
|
||||
},
|
||||
[filter.valueLabelMapping],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full flex-col gap-2">
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => onClearFilter(filter.key)}
|
||||
className={`absolute right-2 top-1/2 z-40 -translate-y-1/2 ${
|
||||
groupSelected.size === 0 ? "hidden" : ""
|
||||
}`}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Popover
|
||||
backdrop="transparent"
|
||||
placement="bottom-start"
|
||||
isOpen={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
>
|
||||
<XCircle className="h-4 w-4 text-default-400" />
|
||||
</Button>
|
||||
<Popover backdrop="transparent" placement="bottom-start">
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md border border-dashed bg-background px-3 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50 dark:bg-prowler-blue-800"
|
||||
startContent={<PlusCircleIcon size={16} />}
|
||||
className="border-input hover:bg-accent hover:text-accent-foreground inline-flex h-auto min-h-10 items-center justify-between whitespace-nowrap rounded-md border border-dashed bg-background px-3 py-2 text-xs font-medium shadow-sm transition-colors focus-visible:outline-none disabled:opacity-50 dark:bg-prowler-blue-800"
|
||||
endContent={
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
}
|
||||
size="md"
|
||||
variant="flat"
|
||||
>
|
||||
<h3 className="text-small">{filter?.labelCheckboxGroup}</h3>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="flex-shrink-0 text-small">
|
||||
{filter?.labelCheckboxGroup}
|
||||
</span>
|
||||
|
||||
{groupSelected.size > 0 && (
|
||||
<>
|
||||
<Divider orientation="vertical" className="mx-2 h-4" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="no-scrollbar hidden max-w-24 space-x-1 overflow-x-auto lg:flex">
|
||||
{groupSelected.size > 3 ? (
|
||||
<span className={filterSelectedClass}>
|
||||
{`+${groupSelected.size - 2} selected`}
|
||||
{selectedValues.length > 0 && (
|
||||
<>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
className="h-4 flex-shrink-0"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-shrink items-center">
|
||||
{selectedValues.length <= 2 ? (
|
||||
<span
|
||||
className="max-w-32 truncate text-xs text-default-500"
|
||||
title={selectedValues.map(getDisplayLabel).join(", ")}
|
||||
>
|
||||
{selectedValues.map(getDisplayLabel).join(", ")}
|
||||
</span>
|
||||
) : (
|
||||
Array.from(groupSelected)
|
||||
.filter((value) => value !== "all")
|
||||
.map((value) => (
|
||||
<div key={value} className={filterSelectedClass}>
|
||||
{value}
|
||||
</div>
|
||||
))
|
||||
<span className="truncate text-xs text-default-500">
|
||||
{isAllSelected
|
||||
? "All selected"
|
||||
: `${selectedValues.length} selected`}
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
onClick={handleClearAll}
|
||||
className="ml-1 flex h-4 w-4 flex-shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-default-200"
|
||||
aria-label="Clear selection"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleClearAll(e as any);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3 text-default-400 hover:text-default-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 dark:bg-prowler-blue-800">
|
||||
<div className="flex w-full flex-col gap-6 p-2">
|
||||
<div className="flex w-full flex-col gap-4 p-2">
|
||||
<CheckboxGroup
|
||||
color="default"
|
||||
label={filter?.labelCheckboxGroup}
|
||||
@@ -184,22 +200,18 @@ export const CustomDropdownFilter: React.FC<CustomDropdownFilterProps> = ({
|
||||
wrapper: "checkbox-update",
|
||||
}}
|
||||
value="all"
|
||||
isSelected={groupSelected.has("all")}
|
||||
onClick={handleSelectAllClick}
|
||||
>
|
||||
Select All
|
||||
</Checkbox>
|
||||
<Divider orientation="horizontal" className="mt-2" />
|
||||
<ScrollShadow
|
||||
hideScrollBar
|
||||
className="flex max-h-96 max-w-56 flex-col gap-y-2 py-2"
|
||||
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
|
||||
>
|
||||
{memoizedFilterValues.map((value) => {
|
||||
// Find the corresponding entity from valueLabelMapping
|
||||
const matchingEntry = filter.valueLabelMapping?.find(
|
||||
{filterValues.map((value) => {
|
||||
const entity = filter.valueLabelMapping?.find(
|
||||
(entry) => entry[value],
|
||||
);
|
||||
const entity = matchingEntry?.[value];
|
||||
)?.[value];
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
|
||||
@@ -229,7 +229,7 @@ export type M365Credentials = {
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
export type GCPCredentials = {
|
||||
export type GCPDefaultCredentials = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
refresh_token: string;
|
||||
@@ -237,6 +237,12 @@ export type GCPCredentials = {
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
export type GCPServiceAccountKey = {
|
||||
service_account_key: string;
|
||||
secretName: string;
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
export type KubernetesCredentials = {
|
||||
kubeconfig_content: string;
|
||||
secretName: string;
|
||||
@@ -246,7 +252,8 @@ export type KubernetesCredentials = {
|
||||
export type CredentialsFormSchema =
|
||||
| AWSCredentials
|
||||
| AzureCredentials
|
||||
| GCPCredentials
|
||||
| GCPDefaultCredentials
|
||||
| GCPServiceAccountKey
|
||||
| KubernetesCredentials
|
||||
| M365Credentials;
|
||||
|
||||
|
||||
@@ -176,6 +176,35 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
|
||||
providerType: z.string(),
|
||||
});
|
||||
|
||||
export const addCredentialsServiceAccountFormSchema = (providerType: string) =>
|
||||
providerType === "gcp"
|
||||
? z.object({
|
||||
providerId: z.string(),
|
||||
providerType: z.string(),
|
||||
service_account_key: z.string().refine(
|
||||
(val) => {
|
||||
try {
|
||||
const parsed = JSON.parse(val);
|
||||
return (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
!Array.isArray(parsed)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
message: "Invalid JSON format. Please provide a valid JSON object.",
|
||||
},
|
||||
),
|
||||
secretName: z.string().optional(),
|
||||
})
|
||||
: z.object({
|
||||
providerId: z.string(),
|
||||
providerType: z.string(),
|
||||
});
|
||||
|
||||
export const testConnectionFormSchema = z.object({
|
||||
providerId: z.string(),
|
||||
runOnce: z.boolean().default(false),
|
||||
|
||||