Compare commits

..
Author SHA1 Message Date
StylusFrost cbe17b0fff test(ui): update GitHub sign-in button locator in E2E tests
- Changed the locator for the GitHub sign-in button in the sign-up page tests to use the input type and value attributes for better accuracy.
- This update enhances the reliability of the E2E tests by ensuring the correct element is targeted during the GitHub OAuth flow.
2025-10-15 16:12:57 +02:00
StylusFrost 73384ef326 test(ui): update GitHub OAuth secrets in E2E test workflow
- Replaced the existing GitHub OAuth client ID and secret with new environment variables for E2E testing.
- This change ensures that the E2E tests use the correct secrets for GitHub OAuth integration, improving test reliability.
2025-10-15 11:54:16 +02:00
StylusFrost 6ad22ec274 test(ui): add GitHub OAuth secrets to E2E test workflow
- Included SOCIAL_GITHUB_OAUTH_CLIENT_ID and SOCIAL_GITHUB_OAUTH_CLIENT_SECRET in the E2E test workflow for GitHub OAuth integration.
- This enhancement supports the recent implementation of GitHub social login in the application.
2025-10-15 11:53:35 +02:00
StylusFrost 512f0bb6df test(ui): implement GitHub OAuth flow for social sign-up
- Added support for GitHub social login in the sign-up process.
- Updated the sign-up page to include a GitHub login button and related methods.
- Enhanced E2E tests to validate the complete GitHub OAuth flow, ensuring users can authenticate and return to the application successfully.
- Updated documentation to reflect the new test case for GitHub social sign-up.
2025-10-15 11:35:34 +02:00
StylusFrost cef7fcc24b Merge branch 'master' into PROWLER-187-create-new-user 2025-10-14 11:03:23 +02:00
StylusFrost fcf42937aa test(ui): enhance session management tests with new helper functions
- Updated the `verifyLogoutSuccess` function to use a regex for URL verification.
- Added new helper functions `getSession` and `verifySessionValid` to streamline session validation in tests, ensuring that session data is correctly retrieved and validated.
2025-10-14 10:26:00 +02:00
Rubén De la Torre VicoandDaniel Barranquero 9761651f8d chore(aws): enhance metadata for cloudfront service (#8829)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-10-14 09:26:33 +02:00
Rubén De la Torre VicoandHugoPBrito 406aace585 chore(aws): enhance metadata for autoscaling service (#8824)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-10-13 16:52:29 +02:00
Rubén De la Torre VicoandHugoPBrito ebd5814112 chore(aws): enhance metadata for backup service (#8826)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-10-13 14:22:49 +02:00
Alan BuscagliaandAlejandro Bailo 42e816081e feat: reusable graph components (#8873)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2025-10-13 13:53:28 +02:00
Alan Buscaglia 741217ce80 feat(ui): API keys implementation (#8874) 2025-10-13 13:48:00 +02:00
Rubén De la Torre VicoandCésar Arroba 5f9ab68bd9 feat(mcp): add GitHub Action to publish MCP Server container to DockerHub (#8875)
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
2025-10-13 10:31:02 +02:00
StylusFrost 2c9d8ad8ea test(ui): simplify sign-up page tests by removing redundant loading state verification
- Removed the `verifyLoadingState` method from the `SignUpPage` class as it was redundant.
- Updated comments in the sign-up test to enhance clarity and focus on key actions.
- Added a call to `verifyNoErrors` to ensure no errors occur during the sign-up process.
2025-10-10 18:21:43 +02:00
Alejandro Bailo fba2854f65 fix(ui): minor bugs (#8898) 2025-10-10 14:56:34 +02:00
Víctor Fernández Poyatos 8794515318 fix(api-keys): make name required and unique (#8891) 2025-10-10 12:35:27 +02:00
Víctor Fernández Poyatos 335db928dc feat(database): add db read replica support (#8869) 2025-10-10 12:27:43 +02:00
StylusFrost f424342e7e test(ui): add E2E tests document for user sign-up flow
- Documented test case details including flow steps, expected results, and key verification points.
- Enhanced existing sign-up test specifications to include relevant tags for better categorization and tracking.
2025-10-10 11:53:18 +02:00
Alejandro Bailo 046baa8eb9 feat(ui): refreshToken implementation (#8864) 2025-10-10 11:02:10 +02:00
Alan Buscaglia ef60ea99c3 fix(api): throw errors for all non-ok responses (#8880) 2025-10-10 10:47:04 +02:00
Hugo Pereira Brito 1483efa18e feat(m365): add M365 certificate auth to API (#8538) 2025-10-10 10:43:11 +02:00
Hugo Pereira Brito b74744b135 feat(m365): add M365 certificate auth to API (#8538) 2025-10-09 16:50:28 +02:00
StylusFrost 9b7e4f59e1 test(ui): implement base page object and enhance sign-up flow tests
- Introduced a BasePage class to encapsulate common functionality for page objects, improving code reusability and maintainability.
- Created new page objects for HomePage, SignInPage, and SignUpPage to streamline the sign-up and login processes in tests.
- Added comprehensive sign-up flow tests to validate user registration and login functionality, ensuring a smooth user experience.
- Updated Playwright configuration to support new test structures and improve overall test organization.
2025-10-09 16:19:27 +02:00
Pepe Fagoaga e80eed6baf chore(ui): remove .env.template (#8887) 2025-10-09 19:06:12 +05:45
Adrián Jesús Peña Rodríguez 1ba22f6f45 feat(api): update role mapping logic in TenantFinishACSView to handle single/manage account users (#8882) 2025-10-09 14:30:26 +02:00
Hugo Pereira Brito da6b7b89cb fix(tests): jira test double lines (#8886) 2025-10-09 13:44:01 +02:00
Hugo Pereira Brito cc9aa7f7ee feat(jira): support of ADF for MarkDown metadata fields (#8878) 2025-10-09 12:31:31 +02:00
Hugo Pereira Brito ecf749fce8 chore(m365): deprecate user auth (#8865) 2025-10-09 12:24:24 +02:00
1a7f52fc9c fix(threatscore): improve the way ThreatScore is calculated (#8582)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-10-09 11:50:10 +02:00
Víctor Fernández Poyatos b630234cdf fix(api-key): use admin connector to validate authentication (#8883) 2025-10-09 11:26:21 +02:00
Víctor Fernández Poyatos d6685eec1f feat(api-keys): support include parameter for entity details (#8876) 2025-10-09 11:14:13 +02:00
StylusFrost d21222aa3a test(ui): format Playwright configuration for consistency
- Added missing commas and improved formatting in the Playwright configuration file to enhance readability and maintain consistency across the codebase.
2025-10-09 11:05:09 +02:00
StylusFrost bdbb2fad78 test(ui): update Playwright test commands to specify project. Compatibility with current e2e test
- Modified Playwright test commands in package.json to explicitly use the 'chromium' project.
2025-10-09 10:54:34 +02:00
StylusFrost cf7b66101c test(ui): enhance Playwright test setups for user authentication
- Added multiple authentication setup files for different user roles (admin, manage scans, manage integrations, etc.) to streamline end-to-end testing.
- Introduced a helper function to authenticate users and save their state for reuse in tests.
- Updated Playwright configuration to include new authentication projects.

This change improves the testing framework by allowing for more comprehensive and role-specific user authentication scenarios.
2025-10-09 10:27:37 +02:00
Pepe Fagoaga 86cff92d1f fix: conventional commit checker (#8879) 2025-10-08 13:19:43 -05:00
Rubén De la Torre Vico 28e81783ef feat(mcp): add API key support for STDIO mode and enhance HTTP mode authentication (#8823) 2025-10-08 15:52:26 +02:00
Rubén De la Torre Vico 13266b8743 feat(mcp): add Prowler Documentation MCP server (#8795) 2025-10-08 12:22:42 +02:00
Rubén De la Torre Vico 4e143cf013 feat(mcp): add HTTP transport support (#8784) 2025-10-08 11:32:39 +02:00
Rubén De la Torre Vico 5cfe140b7b fix(mcp): accept string type for all parameter types in MCP server (#8866) 2025-10-08 10:31:57 +02:00
Hugo Pereira Brito c7d7ec9a3b fix: add pagination for m365 and azure users retrieval (#8858) 2025-10-08 09:07:18 +02:00
Rubén De la Torre VicoandDaniel Barranquero 155a1813cc chore(aws): enhance metadata for cloudformation service (#8828)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-10-07 16:39:23 +02:00
Rubén De la Torre VicoandVíctor Fernández Poyatos 71e444d4ae chore: improve API docs for Provider endpoints (#8723)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-10-07 15:30:14 +02:00
Víctor Fernández Poyatos 42b7f0f1a9 fix(migrations): API key RLS migration (#8863) 2025-10-07 12:39:30 +02:00
Josema Camacho 5b3f0fbd7f fix(doc): document about using the same .env as the code version (#8804) 2025-10-07 09:38:20 +02:00
Josema Camacho 06eb69e455 chore(security): update Django to 5.1.13 (#8842) 2025-10-07 09:38:11 +02:00
338a11eaaf chore(aws): enhance metadata for account service (#8715)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-10-06 12:27:47 -05:00
Alejandro Bailo 8814a0710a fix(scans): detail drawer fails after dependencies migration (#8856) 2025-10-06 17:52:38 +02:00
Chandrapal BadshahandChandrapal Badshah 76a55cdb54 fix: remove maxTokens for gpt-5 (#8843)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-10-06 17:25:20 +02:00
Rubén De la Torre VicoandHugoPBrito 736badb284 chore(aws): enhance metadata for appstream service (#8789)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-10-06 15:29:06 +02:00
Prowler Botandprowler-bot 37f77bb778 chore(regions_update): Changes in regions for AWS services (#8847)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-10-06 08:23:03 -05:00
Daniel Barranquero 7e5e48c588 fix(changelog): duplicated v5.12.4 in SDK changelog (#8852) 2025-10-06 08:22:15 -05:00
Hugo Pereira Brito 5f0017046f chore(findings): change References display in UI (#8793) 2025-10-06 14:04:20 +02:00
Víctor Fernández Poyatos 612d867838 fix(tests): Race condition on redundant API unit test (#8849) 2025-10-06 12:42:16 +02:00
Rubén De la Torre Vico 8c2668ebe4 chore: rename docs AGENTS (#8846) 2025-10-06 10:53:17 +02:00
Rubén De la Torre Vico be4b1bd99b chore: add first version of AGENTS.md (#8799) 2025-10-06 10:47:51 +02:00
Daniel Barranquero 502525eff1 fix(compliance): generate file extension correctly (#8791) 2025-10-06 10:27:16 +02:00
09b5afe9c3 chore(aws): enhance metadata for awslambda service (#8825)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-10-03 13:48:55 +02:00
Víctor Fernández Poyatos 9a4fc784db feat(api-keys): Add API Key support for the Prowler API (#8805) 2025-10-03 13:42:43 +02:00
Rubén De la Torre VicoandHugoPBrito 04177db648 chore(aws): enhance metadata for apigateway service (#8788)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-10-03 11:49:33 +02:00
Alejandro Bailo 2408dbf855 chore(ui): upgrade zod v4, zustand v5, and ai sdk v5 (#8801) 2025-10-03 09:57:46 +02:00
Pepe Fagoaga 9c4a8782e4 fix(conflict-checker): fail on conflict (#8840) 2025-10-03 13:11:45 +05:45
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>César Arroba
0d549ea39e chore(deps): bump github/codeql-action from 3.29.7 to 3.30.5 (#8812)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: César Arroba <cesar@prowler.com>
2025-10-02 10:36:02 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0060081cad chore(deps): bump peter-evans/repository-dispatch from 3.0.0 to 4.0.0 (#8821)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:35:02 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0c2d06dd9a chore(deps): bump actions/setup-node from 4.4.0 to 5.0.0 (#8819)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:34:21 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 14b9be4c47 chore(deps): bump tj-actions/changed-files from 46.0.5 to 47.0.0 (#8814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:33:13 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6bac5650e6 chore(deps): bump aws-actions/configure-aws-credentials from 4.2.1 to 5.0.0 (#8813)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:32:55 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6170462a61 chore(deps): bump actions/github-script from 7.0.1 to 8.0.0 (#8820)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:32:10 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2ad5926b13 chore(deps): bump actions/setup-python from 5.6.0 to 6.0.0 (#8818)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:31:20 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a6ddc85e4c chore(deps): bump codecov/codecov-action from 5.4.3 to 5.5.1 (#8811)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:30:27 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> aceff35f29 chore(deps): bump peter-evans/find-comment from 3.1.0 to 4.0.0 (#8817)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:29:46 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3ae96c3aa6 chore(deps): bump actions/labeler from 5.0.0 to 6.0.1 (#8816)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:28:56 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0dcaaa9083 chore(deps): bump actions/cache from 4.2.3 to 4.3.0 (#8815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:28:43 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 323a7f0349 chore(deps): bump docker/login-action from 3.4.0 to 3.6.0 (#8810)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:25:49 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 736cbea862 chore(deps): bump softprops/action-gh-release from 2.3.2 to 2.3.3 (#8809)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:25:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d3e290978e chore(deps): bump actions/checkout from 4.2.2 to 5.0.0 (#8808)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:24:41 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9c91cfcb7d chore(deps): bump trufflesecurity/trufflehog from 3.90.2 to 3.90.8 (#8807)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-02 10:23:41 +02:00
Daniel Barranquero e279f7fcfd fix: handle eks cluster version and listener certificate arn not in acm (#8802) 2025-10-01 13:55:26 -04:00
Hugo Pereira Brito a555cffebe fix(html): preserve markdown formatting in read-more functionality (#8803) 2025-10-01 13:48:20 -04:00
César Arroba 49f5435392 chore(gha): check API changes for versioning (#8532) 2025-10-01 15:32:08 +02:00
Rubén De la Torre VicoandHugoPBrito a087dd9b85 chore(aws): enhance metadata for accessanalyzer service (#8688)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-10-01 15:05:44 +02:00
Rubén De la Torre VicoandDaniel Barranquero 6e89c301b2 chore(aws): enhance metadata for athena service (#8790)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-10-01 13:59:03 +02:00
Pedro Martín d5dac448a6 fix(m365): add framework and name for iso27001 (#8792) 2025-10-01 13:43:55 +02:00
Pepe Fagoaga 00e6eb35f1 fix(workflows): load latest SDK only for master (#8796) 2025-10-01 13:35:43 +05:45
Hugo Pereira BritoandMrCloudSec cdb455b2b1 feat(aws): add new check ec2_instance_with_outdated_ami (#6910)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-09-30 13:54:36 -04:00
Sergio Garcia 837c65ba23 chore(securityhub): improve logging for Security Hub integration (#8608) 2025-09-30 10:36:42 -04:00
OlmeNavangelolmn <e.angelolm#go.ugr.es>César ArrobaPepe Fagoaga
035293b612 feat: Verify that the CheckID is the same as the filename and classname in the Check class (#8690)
Co-authored-by: angelolmn <e.angelolm#go.ugr.es>
Co-authored-by: César Arroba <cesar@prowler.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-09-30 13:46:59 +02:00
250b5df836 chore(aws): enhance metadata for acm service (#8716)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-30 13:33:09 +02:00
Josema Camacho ec59dbc6ee fix: move delete user 500 error fix to its right version (#8787) 2025-09-30 10:56:29 +02:00
4d5676f00e feat: upgrade to React 19, Next.js 15, React Compiler, HeroUI and Tailwind 4 (#8748)
Co-authored-by: Alan Buscaglia <alanbuscaglia@MacBook-Pro.local>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: César Arroba <cesar@prowler.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2025-09-30 09:59:51 +02:00
2a4b62527a fix(tests_iam): AWS managed policies are isolated (#8609)
Co-authored-by: MustafaAamir <mustafa@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-09-30 13:44:03 +05:45
Josema Camacho ec0341c696 fix(user): PermissionError, 500, when deleting user (#8731) 2025-09-30 09:49:33 +02:00
Rubén De la Torre VicoandDaniel Barranquero 2e5f3a5a66 feat(aws): enhance metadata for apigatewayv2 service (#8719)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-29 12:35:05 -04:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Adrián Jesús Peña Rodríguez
231a5fab86 chore(deps-dev): bump authlib from 1.6.1 to 1.6.4 (#8741)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-09-29 12:08:47 -04:00
Andoni Alonso 10319ea69d docs(github): refactor getting started and auth (#8767) 2025-09-29 11:33:15 -04:00
Sergio GarciaandAndoni A. 53bb5aff22 feat(llm): add LLM provider (#8555)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-09-29 11:24:10 -04:00
Rubén De la Torre VicoandDaniel Barranquero 52a5fff61f chore(aws): enhance metadata for appsync service (#8721)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-29 16:36:43 +02:00
Andoni Alonso f28754b883 docs(iac): refactor getting started and auth (#8779) 2025-09-29 15:41:25 +02:00
Pedro Martín 6fce797ca2 feat(compliance-mapper): add first version (#8568) 2025-09-29 15:40:29 +02:00
Adrián Jesús Peña RodríguezandPepe Fagoaga a1fd315104 ref(actions): remove xmlsec step (#8482)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-09-29 13:04:33 +02:00
Prowler Botandprowler-bot a91f0ac8b5 chore(regions_update): Changes in regions for AWS services (#8777)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-09-29 16:27:27 +05:45
Andoni Alonso 2c96df05f4 docs(mongodbatlas): refactor getting started and auth (#8776) 2025-09-29 11:58:09 +02:00
Chandrapal BadshahandChandrapal Badshah b57788c7b9 fix: update prowler package version in api (#8778)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-29 11:44:45 +02:00
Pedro MartínandAndoni A. 7431bab2a7 docs(threatscore): add info with Prowler ThreatScore (#8711)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-09-29 11:17:05 +02:00
Andoni Alonso a52697bfdf docs(m365): refactor getting started and auth (#8761) 2025-09-29 10:01:40 +02:00
Alejandro Bailo 9dc2199381 feat(ui): add compliance_name (#8775) 2025-09-29 09:59:18 +02:00
Rubén De la Torre Vico 89db760b89 docs(mcp): add preview feature disclaimer (#8774) 2025-09-29 09:42:16 +02:00
Chandrapal BadshahandChandrapal Badshah 4356c1e186 fix(ui): update ui changelog (#8771)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-26 17:08:17 +02:00
Rubén De la Torre Vico e32cebc553 feat(mcp): add Dockerfile for MCP Server containerization (#8768) 2025-09-26 15:04:24 +02:00
Andoni AlonsoandRubén De la Torre Vico 23e1cc281d docs(azure): refactor getting started and auth (#8754)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
2025-09-26 15:02:57 +02:00
Josema Camacho 48d3fb4fe3 feat(doc): 📚 add documenation about JWT keys autogeneration (#8766) 2025-09-26 13:52:46 +05:45
César Arroba ab727e6816 chore(gha): fix e2e workflow (#8769) 2025-09-25 22:13:53 +05:45
Rubén De la Torre Vico 23d882d7ab feat(mcp): add Prowler App MCP Server (#8744) 2025-09-25 15:21:34 +02:00
Alejandro Bailo 59435167ea fix(scans): update link disable condition for findings table (#8762) 2025-09-25 12:57:22 +02:00
Andoni Alonso 77cdd793f8 fix(aws): cover SNS ResourceID in Quick Inventory output (#8763) 2025-09-25 11:14:32 +02:00
Andoni Alonso d13f3f0e0c docs(gcp): refactor getting started and auth (#8758) 2025-09-25 10:19:01 +02:00
Víctor Fernández Poyatos 56821de2f4 feat(tasks): Move compliance tasks to compliance queue (#8755) 2025-09-24 14:00:17 +02:00
Daniel BarranqueroandAndoni A. 92190fa69f feat(docs): add renaming checks to developer guide (#8717)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-09-24 11:46:52 +02:00
Prowler Botandprowler-bot 85db7c5183 chore(regions_update): Changes in regions for AWS services (#8736)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-09-24 10:38:12 +02:00
Josema Camacho a55ac266bf chore(django): update django to 5.1.12 due to security problems (#8693) 2025-09-23 16:35:25 +05:45
Andoni Alonso 90622e0437 docs: update Entra SSO SAML video link (#8745) 2025-09-23 12:43:51 +02:00
Pepe Fagoaga 81596250dc fix(actions): lock poetry after changes (#8477) 2025-09-23 14:31:45 +05:45
Rubén De la Torre Vico 43db5fe527 feat(mcp): add basic logger (#8740) 2025-09-23 09:09:38 +02:00
Pepe Fagoaga dfb479fa80 chore(readme): remove deprecations and fix typo (#8739) 2025-09-22 20:31:42 +05:45
Pedro Martín aa88b453ff fix(compliance): change order in models and remove prints (#8738) 2025-09-22 15:45:09 +02:00
Pedro MartínandPepe Fagoaga fbda66c6d1 feat(compliance): add name for each compliance (#7920)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-09-22 14:53:27 +02:00
Adrián Jesús Peña Rodríguez 2200e65519 feat(auth): add safeguards to prevent self-role removal and enforce MANAGE_ACCOUNT role presence (#8729) 2025-09-22 14:04:39 +02:00
Josema Camacho b8537aa22d feat(config): add generation for JWT keys if missing (#8655) 2025-09-22 13:14:54 +02:00
Rubén De la Torre Vico cb4a5dec79 chore: set an appropiate User-Agent in requests (#8724) 2025-09-22 12:48:13 +02:00
Rubén De la Torre Vico 0286de7ce2 chore: add mcp_server component labeler configuration (#8737) 2025-09-22 15:40:23 +05:45
b00602f109 fix(users): only list roles and memberships with manage_account (#8281)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-22 11:25:24 +02:00
Adrián Jesús Peña Rodríguez 1cfae546a0 chore(deps): add markdown package version 3.9 to dependencies (#8735) 2025-09-22 10:44:26 +02:00
Sergio Garcia 05dae4e8d1 fix(iac): handle empty results (#8733) 2025-09-16 14:20:15 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>MrCloudSec
52ddaca4c5 chore(deps-dev): bump moto from 5.0.28 to 5.1.11 (#7100)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-09-16 14:17:47 +02:00
Alejandro BailoandPepe Fagoaga 940a1202b3 fix: handle 4XX and 204 properly (#8722)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-09-15 17:07:15 +02:00
ec27451199 chore(regions_update): Changes in regions for AWS services (#8728)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-09-15 15:02:37 +02:00
Sergio Garcia 60e06dcc6e chore(html): support markdown in HTML (#8727) 2025-09-15 11:38:18 +02:00
Hugo Pereira Britoandalejandrobailo 7733aab088 feat: add additional_urls to finding details and markdown (#8704)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-15 11:33:27 +02:00
Pepe Fagoaga 5c6fadcfe7 chore(changelog): remove whitespace in links (#8712) 2025-09-12 17:09:19 +05:45
César Arroba 1bdb314e2c chore(gha): permissions missed for conflict checker action (#8714) 2025-09-12 12:37:12 +02:00
Rubén De la Torre Vico 5b0365947f feat: add first Prowler MCP server version (#8695) 2025-09-12 09:56:36 +02:00
Daniel BarranqueroandSergio Garcia b512f6c421 fix(firehose): false positive in firehose_stream_encrypted_at_rest (#8599)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-09-11 09:55:16 -04:00
Alejandro Bailo c4a8771647 chore(dependencies): update package versions and track them (#8696) 2025-09-11 15:36:06 +02:00
Alejandro Bailo 6f967c6da7 fix(auth): validate email field (#8698) 2025-09-11 15:29:49 +02:00
Alejandro Bailo 82cd29d595 fix(auth): add method attribute to form for proper submission handling (#8699) 2025-09-11 15:02:36 +02:00
Daniel Barranquero 14c2334e1b fix(defender): change policies rules key (#8702) 2025-09-11 13:46:21 +02:00
Rubén De la Torre VicoandHugoPBrito 3598514cb4 chore(aws/config): adapt metadata to new standarized format (#8641)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-09-10 17:46:11 +02:00
Hugo Pereira Brito c4ba061f30 chore(outputs): adapt to new metadata specification (#8651) 2025-09-10 17:21:19 +02:00
Chandrapal BadshahandChandrapal Badshah f4530b21d2 fix(lighthouse): make Enter submit text (#8664)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-10 16:34:35 +02:00
Chandrapal BadshahandChandrapal Badshah 3949ab736d fix(lighthouse): allow scrolling during AI response streaming (#8669)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-10 16:34:24 +02:00
sumit-tftandalejandrobailo 9da5066b18 feat(ui): add copy link icon to finding detail page (#8685)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-10 16:30:16 +02:00
941539616c chore(aws/neptune): adapt some metadata fields to new format (#8494)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-09-10 16:21:30 +02:00
sumit-tftandalejandrobailo 135fa044b7 feat(ui): Add Prowler Hub menu item with tooltip (#8692)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-10 16:09:09 +02:00
Andoni Alonso 48913c1886 docs(aws): refactor getting started and auth (#8683) 2025-09-10 13:45:36 +02:00
Pedro Martín ea20943f83 feat(actions): support dashboard changes in changelog (#8694) 2025-09-10 11:05:56 +02:00
Hugo Pereira Brito 2738cfd1bd feat(dashboard): add Description and markdown support (#8667) 2025-09-10 10:53:53 +02:00
Rubén De la Torre VicoandHugoPBrito 265c3d818e docs(developer-guide): enhance check metadata format (#8411)
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
2025-09-10 09:19:08 +02:00
c0a9fdf8c8 docs(jira): add comprehensive guide for Jira integration in Prowler App (#8681)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-09-09 17:01:12 +02:00
Rubén De la Torre Vico 8b3335f426 chore: add metadata-review label for .metadata.json files (#8689) 2025-09-09 20:32:04 +05:45
Daniel Barranquero 252033d113 fix(compliance): replace old check id with new one (#8682) 2025-09-09 14:25:56 +02:00
Prowler Botandprowler-bot 0bc00dbca4 chore(release): Bump version to v5.13.0 (#8679)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-09-09 16:36:22 +05:45
Adrián Jesús Peña Rodríguez 3f5178bffb chore: update api changelog (#8677) 2025-09-09 10:23:55 +02:00
Josema Camacho e08b272a1d fix(login): add DRF throttle option for dj-rest-auth lib (#8672) 2025-09-09 09:34:02 +02:00
Pedro Martín 64c43a288d feat(jira): add force accept language for requests (#8674) 2025-09-09 13:17:25 +05:45
Daniel Barranquero 74bf0e6b47 fix(aws): nonetype errors in opensearch, firehose and cognito (#8670) 2025-09-09 13:12:57 +05:45
Andoni AlonsoandDaniel Barranquero 02b7c5328f docs: update providers table (#8676)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-09 09:25:20 +02:00
Alejandro Bailo bb02004e7c fix: social auth buttons showed for sign-up (#8673) 2025-09-09 09:23:56 +02:00
Andoni AlonsoandDaniel Barranquero 82cf216a74 feat(mongodbatlas): add MongoDB Atlas provider PoC (#8312)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-09 09:18:37 +02:00
Daniel Barranquero 7916425ed4 fix(memorydb): handle clusters with no security groups (#8666) 2025-09-08 15:05:13 -04:00
Andoni Alonso d98063ed47 docs: add interface column to providers (#8675) 2025-09-08 15:03:17 -04:00
Andoni Alonso 27bf78a3a1 docs: update providers list (#8671) 2025-09-08 17:12:16 +02:00
Andoni Alonso f50bd50d60 docs: add SSO with SAML Entra ID video link (#8668) 2025-09-08 14:57:38 +02:00
Alejandro Bailo 80665e0396 feat(ui): send a finding to Jira (#8649) 2025-09-08 14:15:23 +02:00
Pedro Martín 4b259fa8dd chore(changelog): update with latest changes (#8665) 2025-09-08 17:24:31 +05:45
Hugo Pereira Brito 10db2ed6d8 chore(docs): add notes regarding gov accounts support (#8656) 2025-09-08 11:07:00 +02:00
Chandrapal BadshahandChandrapal Badshah 422a8a0f62 fix: change title in lighthouse settings (#8615)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-08 10:34:09 +02:00
Daniel Barranquero 906a2cc651 fix(entra): add metadata description for check entra_admin_users_phishing_resistant_mfa_enabled (#8654) 2025-09-08 08:11:46 +02:00
Víctor Fernández Poyatos 43fe9c6860 feat(integrations): allow sending findings to Jira from the API (#8645) 2025-09-05 14:28:34 +02:00
Andoni Alonso f87b2089fb docs: remove llms.txt (#8653) 2025-09-05 17:08:42 +05:45
Samuele PasiniandDaniel Barranquero 1884874ab6 fix: typo ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_* CheckID (#8294)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-09-05 13:16:12 +02:00
Andoni Alonso cd6d29e176 docs: reorg tutorials (#8652) 2025-09-05 16:49:14 +05:45
Pedro Martín 0b7055e983 feat(jira): add send_finding method with specific finding fields (#8648) 2025-09-05 12:25:53 +02:00
Josema Camacho ae53b76d78 feat(login): add DJANGO_THROTTLE_TOKEN_OBTAIN to main .env file (#8650) 2025-09-05 16:01:48 +05:45
Josema Camacho 406e473b5c feat(login): add throttling option for the /api/v1/tokens endpoint (#8647) 2025-09-05 14:37:31 +05:45
Pedro Martín 1a2bf461f0 feat(jira): support labels in jira tickets (#8603) 2025-09-05 09:53:24 +02:00
Samuele Pasiniandpedrooot 1b49c0b27f feat: add --excluded-checks-file flag (#8301)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2025-09-05 09:33:21 +02:00
Pablo LaraandAdrián Jesús Peña Rodríguez 12ada66978 feat: add status filter to /overviews endpoint (#8186)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-09-04 18:46:14 +02:00
Alejandro Bailo daa2536005 feat: Jira UI integration - pages and server actions (#8640) 2025-09-04 15:59:37 +02:00
Chandrapal BadshahandChandrapal Badshah 69a62db19a chore: rename to lighthouse ai (#8614)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-04 15:30:07 +05:45
Pedro MartínandHugo Pereira Brito 79450d6977 fix(securityhub): resolve TypeError from Python3.9 (#8619)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-09-03 17:52:09 +02:00
Víctor Fernández Poyatos 0463fd0830 refactor(integrations-jira): Move domain to credentials and retrieve metadata during connection test (#8637) 2025-09-03 17:24:42 +02:00
Alejandro Bailo b15e3d339c fix(saml): remove validation call on email domain change (#8638) 2025-09-03 17:04:51 +02:00
Pedro Martín 1fc12952ba feat(jira): add color for manual status (#8642) 2025-09-03 16:53:31 +02:00
sumit-tftandalejandrobailo 088a6bcbda feat(ui): handle no-permissions on scan page (#8624)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-03 15:51:14 +02:00
Hugo Pereira Brito a3b0bb6d4b refactor(models): rename AdditionalUrls to AdditionalURLs (#8639) 2025-09-03 19:34:06 +05:45
Pedro Martín 3c819f8875 chore(changelog): update with latest changes (#8636) 2025-09-03 12:54:50 +02:00
Pedro Martín cdf0292bbc feat(jira): add get_metadata (#8630) 2025-09-03 10:59:07 +02:00
César Arroba 987121051b chore(sdk): comment push readme to dockerhub steps (#8628) 2025-09-02 21:48:42 +05:45
Hugo Pereira Brito c9ed7773d2 feat(models): add AdditionalUrls field to check metadata (#8590) 2025-09-02 21:27:21 +05:45
Pepe Fagoaga fdf45aac51 fix(img): prowler architecture (#8635) 2025-09-02 21:15:40 +05:45
Alejandro Bailo 3ded224a4b fix: new errors detected through the app (#8629) 2025-09-02 12:35:06 +02:00
sumit-tft 230a085c76 fix(ui): display NoProvidersAdded when no cloud providers are configured (#8626) 2025-09-02 12:33:58 +02:00
Chandrapal BadshahandChandrapal Badshah 8cd90e07dc chore(ui): eslint nextjs files (#8627)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-09-02 12:15:48 +02:00
Pedro Martín 06ded98d05 feat(jira): add data to table and error handling (#8601) 2025-09-02 11:48:52 +02:00
Pedro Martín a5066326bd chore(changelog): update with latests changes (#8620) 2025-09-02 11:27:13 +02:00
Alejandro Bailo 83a9ac2109 chore(ui): update CHANGELOG (#8625) 2025-09-02 10:45:34 +02:00
Alejandro Bailo 136eb4facd feat: 50X errors handler (#8621) 2025-09-02 10:12:03 +02:00
Víctor Fernández Poyatos d4eb4bdca7 feat(integrations): Support JIRA integration in the API (#8622) 2025-09-02 09:53:36 +02:00
Alejandro Bailo 665c9d878a chore(ui): update Next.js and ESLint dependencies to version 14.2.32 (#8623) 2025-09-01 18:38:39 +02:00
Hugo Pereira Britoandalejandrobailo a064e43302 chore(ui): render attributes as markdown (#8604)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-09-01 16:43:36 +02:00
Daniel Barranquero fdb76e7820 feat(docs): update mfa enforcement date for m365 (#8610) 2025-09-01 09:48:21 +02:00
Pepe Fagoaga 1259bb85e3 fix: remove dot (#8613) 2025-08-29 14:46:19 +05:45
Pepe Fagoaga 0db9ab91b2 chore(docs): review stats, imgs and update copy (#8612) 2025-08-29 14:44:01 +05:45
César Arroba f6ea314ec0 chore(sdk): push readme file to docker hub (#8611) 2025-08-29 14:43:53 +05:45
9e02da342b docs: Security Hub API and UI documentation (#8576)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-08-28 20:43:42 +05:45
Prowler Botandprowler-bot 358d4239c7 chore(release): Bump version to v5.12.0 (#8605)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-28 16:56:24 +02:00
Víctor Fernández Poyatos b003fca377 fix(docs): remove empty sections (#8600) 2025-08-28 12:55:46 +02:00
Víctor Fernández Poyatos b4deda3c3f docs(api): fix API response samples (#8592) 2025-08-28 12:39:07 +02:00
Sergio Garcia 338bb74c0c fix(azure): query API management logs with not empty operations (#8598) 2025-08-28 12:03:35 +02:00
Alejandro Bailo 7342a8901f chore: update CHANGELOG.md for Prowler v5.11.0 release (#8597) 2025-08-28 11:43:24 +02:00
Sergio GarciaandRubén De la Torre Vico f484b83f15 feat(azure): Add APIM threat detection for LLM jacking attacks (#8571)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
2025-08-28 11:42:07 +02:00
Adrián Jesús Peña Rodríguez c69187f484 chore: prepare api changelog for 5.11 (#8596) 2025-08-28 10:25:08 +02:00
Alejandro Bailo 5038afeb26 fix(security-hub): copy updated (#8594) 2025-08-27 18:42:34 +02:00
Sergio Garcia fce43cea16 chore: update changelog (#8593) 2025-08-27 17:57:07 +02:00
Andoni Alonso 43a14b89bc fix(github): provider always scans user instead of organization when using provider UID (#8587) 2025-08-27 17:45:13 +02:00
TomandSergio Garcia 24364bd73e feat(gcp): Add support for skipping APIs check (#8575)
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
2025-08-27 14:44:34 +02:00
Adrián Jesús Peña Rodríguez a1abe6dd2d fix(sh): reset regions information if connection fails (#8588) 2025-08-27 14:15:09 +02:00
César Arroba 25098bc82a chore(gha): fix conflict checker action (#8586) 2025-08-27 13:41:39 +02:00
sumit-tftandalejandrobailo 20f2f45610 feat(ui): add S3 bucket link with folder for each integration (#8554)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-08-27 12:40:37 +02:00
Alejandro Bailo 06c2608a05 feat(integrations): external links and copies changed (#8574) 2025-08-27 12:40:25 +02:00
Alejandro Bailo 329ac113f2 chore(docs): update CHANGELOG properly (#8585) 2025-08-27 11:57:12 +02:00
Hugo Pereira Brito 97179d2b43 fix(docs): incorrect permission in sp creation guide (#8581) 2025-08-27 11:01:37 +02:00
sumit-tft 8317ea783f feat(ui): show all provider UIDs in scan page filter regardless of co… (#8375) 2025-08-27 10:50:16 +02:00
Andoni Alonso 65e7e89d61 fix(github): GitHub Personal Access Token authentication fails without user:email scope (#8580) 2025-08-27 09:57:32 +02:00
Víctor Fernández Poyatos 26a4dd4e8d chore: bump h2 to 4.3.0 (#8573) 2025-08-26 15:17:06 +02:00
Alejandro Bailo dab0cea2dd feat(ui): Security Hub (#8552) 2025-08-26 14:30:45 +02:00
Daniel Barranquero 3b42eb3818 fix(s3): resource metadata error in s3_bucket_shadow_resource_vulnerability (#8572) 2025-08-26 13:30:49 +02:00
Prowler Botandprowler-bot a5ba950627 chore(regions_update): Changes in regions for AWS services (#8567)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-26 09:57:45 +02:00
Andoni Alonso a1232446c1 docs: refactor several sections (#8570) 2025-08-26 09:55:18 +02:00
Pedro MartínandAndoni A. aa6f851887 docs(aws): deploying prowler iam roles across aws organizations (#8427)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-08-26 09:45:14 +02:00
Adrián Jesús Peña Rodríguez 25f972e910 feat(sh): create asff of there is an enabled SecurityHub integration (#8569) 2025-08-25 16:58:21 +02:00
Pedro Martín 7216e5ce3d chore(github): improve pull request template (#7910) 2025-08-25 16:22:55 +02:00
Adrián Jesús Peña Rodríguez 83242da0ab feat(integrations): implement AWS Security Hub integration (#8365) 2025-08-25 15:53:48 +02:00
Alejandro Bailo d457166a0c fix(ui): AWS form selector default values (#8553) 2025-08-25 12:30:02 +02:00
Daniel Barranquero 88f38b2d2a feat(docs): remove old requirements links (#8561) 2025-08-22 14:22:50 +02:00
Pepe Fagoaga c2e0849d5f fix(conflict-checker): use prowler-bot (#8560) 2025-08-22 17:27:44 +05:45
Andoni Alonso 1fdebfa295 docs: remove "Requirements" page (#8559) 2025-08-22 15:55:25 +05:45
Sergio GarciaandAdrián Jesús Peña Rodríguez ea6d04ed3a chore(securityhub): add static credentials and role assumption support (#8539)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-08-22 11:58:35 +02:00
Sergio Garcia 2167683851 feat(aws): add Resource Explorer enumeration actions (#8557) 2025-08-22 11:47:51 +02:00
Pepe Fagoaga 6324be31ab fix(api): poetry lock up to date with the SDK (#8558) 2025-08-22 11:05:14 +02:00
Alejandro Bailo 525f152e51 fix(ui): update authorization logic to match right paths (#8556) 2025-08-22 10:35:28 +02:00
Sergio GarciaandAndoni A. c3a2d79234 chore(iac): change engine to trivy (#8466)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-08-22 10:17:51 +02:00
Andoni Alonso cefa708322 docs: add provider bulk provisioning (#8551) 2025-08-21 16:33:45 +02:00
Andoni Alonso 1a9e14ab2a chore(bulk-provisioning-tool): add script to bulk provision providers (#8540) 2025-08-21 13:11:46 +02:00
Chandrapal BadshahandChandrapal Badshah b1c6094b6d fix: Remove temperature for GPT-5 models (#8550)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-21 12:40:49 +02:00
Pablo Lara 1038b11fe3 docs: update changelog (#8549) 2025-08-21 12:22:27 +02:00
Chandrapal BadshahandChandrapal Badshah d54e3b25db fix: Refactor getting lighthouse config (#8546)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-21 11:14:21 +02:00
Pepe Fagoaga 6a8e8750bb chore(actions): conflict checker (#8547) 2025-08-21 14:28:18 +05:45
Hugo Pereira Brito ad3d4536fb fix(m365): only evaluate enabled users in entra_users_mfa_capable (#8544) 2025-08-20 16:45:00 +02:00
Andoni Alonso 46c24055ee docs: refactor Overview into several files (#8543) 2025-08-20 17:44:06 +05:45
Pepe Fagoaga 4c6a1592ac chore(actions): update docs comment with link (#8448) 2025-08-20 17:42:32 +05:45
Hugo Pereira BritoandMrCloudSec 89e657561c feat(github): add User Email and APP name/installations information (#8501)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-20 12:26:38 +02:00
Hugo Pereira BritoandMrCloudSec 55099abc86 fix(organization): list all accessible organizations (#8535)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-20 12:13:01 +02:00
Andoni Alonso 3c599a75cc feat(iam): add ECS privilege escalation patterns to IAM checks (#8541) 2025-08-20 09:23:30 +02:00
f77897f813 feat: gpt-5 and gpt-5-mini integration with lighthouse (#8527)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-08-19 16:49:21 +02:00
Sergio Garcia 30518f2e0e feat(aws): new check eks_cluster_deletion_protection_enabled (#8536) 2025-08-19 10:25:24 +02:00
Chandrapal BadshahandChandrapal Badshah efdeb431ba feat: Add resource agent to supervisor (#8509)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-19 09:40:14 +02:00
Sergio Garcia bb07cf9147 fix(aws): exact match in resource-arn filtering (#8533) 2025-08-18 12:11:13 +02:00
Prowler Botandprowler-bot 9214b5c26f chore(regions_update): Changes in regions for AWS services (#8531)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-18 11:58:41 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d57df3cc28 chore(deps): bump actions/upload-artifact from 4.5.0 to 4.6.2 (#8154)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-18 11:43:41 +02:00
Andoni Alonso 2f5fce41dc feat(iam): remove standalone iam:PassRole from privesc detection and add missing patterns (#8530) 2025-08-18 11:35:14 +02:00
Chandrapal BadshahandChandrapal Badshah 6918a75449 fix: add business context to lighthouse chat (#8528)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-18 09:49:23 +02:00
Pablo Lara 3aeaa3d992 feat(filters): improve provider connection filter UX (#8520) 2025-08-18 09:10:16 +02:00
Sergio Garcia fd833eecf0 fix(github): solve Github APP auth method (#8529) 2025-08-18 08:35:19 +02:00
Andoni Alonso 39e4d20b24 feat(iam): add Bedrock AgentCore privilege escalation combo (#8526) 2025-08-15 13:25:15 +02:00
Sergio Garcia dfdd45e4d0 fix(github): list all accessible repositories (#8522) 2025-08-14 10:38:38 +02:00
Hugo Pereira Brito 81478dfed3 fix(compliance): GitHub CIS 1.0 (#8519) 2025-08-13 16:45:36 +02:00
Chandrapal BadshahandChandrapal Badshah 2854f8405c fix: simplify error handling to use only error.message (#8518)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-13 10:59:47 +02:00
Jaen-923andghkim583 0e1578cfbc chore(aws): Refine kisa isms-p compliance mapping (#8479)
Co-authored-by: ghkim583 <203069125+ghkim583@users.noreply.github.com>
2025-08-13 09:08:37 +02:00
Hugo Pereira Brito f5b1532647 fix(kafka): false positives in kafka_cluster_is_public check (#8514) 2025-08-13 09:05:09 +02:00
Sergio Garcia d9f3a6b88e docs(github): add Github onboarding documentation (#8510) 2025-08-12 17:11:30 +02:00
Hugo Pereira BritoandSergio Garcia b0c386fc60 fix(app): fix false positives in app_http_logs_enabled (#8507)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-08-12 14:47:17 +02:00
Hugo Pereira Brito 72b06261df fix(storage): fall positives in storage_geo_redundant_enabled (#8504) 2025-08-12 12:30:43 +02:00
sumit-tftandPablo Lara 1562b77581 fix(ui): redirection after deleting providers group and improve erro… (#8389)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-08-12 11:31:45 +02:00
Daniel Barranquero 10e38ca407 fix: missing resource_name in GCP and Azure Defender checks (#8352) 2025-08-11 16:16:08 +02:00
Rubén De la Torre VicoandMrCloudSec 5842f2df37 feat(azure/vm): add new check vm_jit_access_enabled (#8202)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-11 13:12:36 +02:00
Prowler Botandprowler-bot 8b3b9ffd99 chore(regions_update): Changes in regions for AWS services (#8499)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-11 12:00:02 +02:00
Rubén De la Torre VicoandMrCloudSec d238050065 feat(azure/vm): add new check vm_sufficient_daily_backup_retention_period (#8200)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-11 11:44:45 +02:00
sumit-tft 5572d476ad fix(ui): adjust table headers to be single-line and consistent (#8480) 2025-08-11 10:47:10 +02:00
sumit-tftandPablo Lara 3c94d3a56f fix(ui): disable See Compliance button until scan completes (#8487)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-08-11 10:37:35 +02:00
Hugo Pereira BritoandSergio Garcia 85af4ff77c feat(m365): add certificate auth method to cli (#8404)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-08-11 09:47:56 +02:00
Daniel Barranquero dcee114ef3 fix: validation errors in azure and m365 (#8368) 2025-08-11 09:42:30 +02:00
Pedro MartínandSergio Garcia 760723874c fix(prowler-threatscore): order the requirements by id (#8495)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-08-11 08:20:10 +02:00
Pedro Martín c0a4898074 chore(changelog): update (#8496) 2025-08-11 07:48:23 +02:00
Alejandro BailoandPablo Lara 03c0533b58 feat(ui): overview charts display improved (#8491)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-08-08 10:59:15 +02:00
sumit-tftandPablo Lara c8dcb0edb0 feat(ui): add GitHub submenu under High Risk Findings (#8488)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-08-08 10:36:36 +02:00
Pablo Lara 82171ee916 docs: update changelog (#8489) 2025-08-08 10:20:53 +02:00
df4bf18b97 feat(ui): add Mutelist menu item under Configuration (#8444)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-08-08 09:09:37 +02:00
Alejandro Bailo 94e60f7329 fix(ui): assume role fields shown (#8484) 2025-08-07 17:44:46 +02:00
Rubén De la Torre VicoandClaude f1ba5abbec chore(docs): update provider statistics in README.md (#8483)
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-07 17:10:56 +02:00
Hugo Pereira BritoandMrCloudSec 6cc1a9a2cb fix(compliance): delete invalid requirements for GitHub CIS 1.0 (#8472)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-07 20:51:20 +07:00
Pablo Lara 31f98092bf feat(ui): add provider type filter to providers page (#8473) 2025-08-07 14:34:04 +02:00
Pepe Fagoaga 85197036ca chore(env): Update NEXT_PUBLIC_PROWLER_RELEASE_VERSION (#8476) 2025-08-07 17:50:18 +05:45
Pepe Fagoaga be43025f00 fix(actions): always get latest SDK reference (#8474) 2025-08-07 17:38:40 +05:45
César Arroba c6b34f0a85 chore(api): open PR with API prowler version (#8475) 2025-08-07 13:49:39 +02:00
Prowler Botandprowler-bot 675698a26a chore(release): Bump version to v5.11.0 (#8470)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-07 12:40:55 +02:00
8d9bf2384f docs: S3 tutorial documentation (#8414)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-08-07 16:04:42 +05:45
César Arroba ff900a2a45 chore(gha): use prowler-bot for push in action (#8469) 2025-08-07 10:50:58 +02:00
César Arroba a41663fb0d chore(gha): fix release preparation workflow (#8468) 2025-08-07 10:41:16 +02:00
César Arroba 033e9fd58c chore(gha): fix release preparation workflow (#8467) 2025-08-07 10:36:22 +02:00
240b02b498 feat(ui): add SAML documentation link in config modal (#8461)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2025-08-07 10:23:07 +02:00
Rubén De la Torre Vico 87eb2dfdf7 chore(changelog): move fixes from version 5.9.3 to 5.10 (#8464) 2025-08-07 13:43:56 +05:45
Alejandro Bailo b4d8d64f0e feat: update AWS role credentials form to set default credentials typ… (#8459) 2025-08-07 09:54:48 +02:00
Pablo Lara 7944ebe83a docs: update changelog (#8462) 2025-08-07 09:39:24 +02:00
Pepe Fagoaga bd138114c9 fix: changelog check update messages (#8465) 2025-08-07 13:22:54 +05:45
Adrián Jesús Peña Rodríguez d527a3f12b chore: update changelog (#8463) 2025-08-07 09:35:16 +02:00
Pepe Fagoaga 260fada3eb fix(s3): Use HeadBucket instead of GetBucketLocation (#8456) 2025-08-06 19:20:52 +05:45
Pepe Fagoaga 0ee0fc082a chore(s3): remove trailing 's' from docs helper (#8458) 2025-08-06 14:21:39 +02:00
Hugo Pereira Brito 9d66d86f66 fix(docs): m365 requirements Needed permissions link (#8457) 2025-08-06 13:51:16 +02:00
Alejandro Bailo 825e53c38f feat(ui): add a default Mutelist placeholder (#8455) 2025-08-06 13:11:31 +02:00
Daniel BarranqueroandSergio Garcia 196c17d44d feat(gcp): add retry to avoid quota limit errors (#8412)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-08-06 16:59:41 +07:00
Andoni AlonsoandSergio Garcia fc69e195e4 fix(github): handle GithubAppIdentityInfo in output generation (#8423)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-08-06 16:55:44 +07:00
Prowler Botandprowler-bot 5f53a9ec6f chore(regions_update): Changes in regions for AWS services (#8437)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-08-06 16:53:43 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5e72a40898 chore(deps): bump github/codeql-action from 3.29.2 to 3.29.5 (#8434)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 16:52:09 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 496ada3cba chore(deps): bump trufflesecurity/trufflehog from 3.89.2 to 3.90.2 (#8433)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 16:51:42 +07:00
Adrián Jesús Peña Rodríguez 481a43f3f6 chore(integrations): remove unnecessary error alerts (#8453) 2025-08-06 09:16:26 +02:00
Pepe Fagoaga 58298706d4 docs(saml): IdP initiated flow (#8435) 2025-08-06 12:46:18 +05:45
Pepe Fagoaga e75a760da0 fix(ui): cfn quick link (#8452) 2025-08-05 22:42:57 +05:45
Pepe Fagoaga c313757ef2 fix(templates): only one cloudformation template (#8451) 2025-08-05 18:17:50 +02:00
Adrián Jesús Peña Rodríguez 284678fe48 fix(export): remove static timestamp (#8449) 2025-08-05 18:12:04 +02:00
Alejandro Bailo c3d25e6f39 feat(ui): S3 integrations pagination added (#8450) 2025-08-05 18:11:32 +02:00
Adrián Jesús Peña Rodríguez a9d16bbbce chore: change output folder (#8447) 2025-08-05 14:07:35 +02:00
Pepe Fagoaga 92bc992e7f feat(s3): templates for permissions (#8395) 2025-08-05 17:36:04 +05:45
Alejandro Bailo 903e4f8b9f feat(integrations): add enabled attribute to S3 integration (#8446) 2025-08-05 13:13:58 +02:00
Alejandro Bailo 2c09076f91 feat: output_directory default value added (#8445) 2025-08-05 12:20:31 +02:00
Adrián Jesús Peña Rodríguez 3d4902b057 feat(integrations): integrations enabled by default (#8439) 2025-08-05 11:25:42 +02:00
Chandrapal BadshahandChandrapal Badshah b30eab7935 fix: Don't invoke tools if no providers or completed scans (#8443)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-05 09:32:35 +02:00
sumit-tftandPablo Lara cf8402e013 feat(ui): add notification system (#8394)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-08-05 09:06:15 +02:00
af8fbaf2cd docs(mutelist): improve mutelist docs across all the providers (#8397)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-08-05 08:38:50 +02:00
Alejandro BailoandPepe Fagoaga c748e57878 feat: manage integration permission behavior (#8441)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-08-04 17:49:04 +02:00
Alejandro Bailo a5187c6a42 feat(ui): S3 integration retouches (#8438) 2025-08-04 16:04:10 +02:00
Alejandro Bailo e19ed30ac7 feat(UI): xml validation (#8429) 2025-08-04 12:09:18 +02:00
Hugo Pereira Brito 96ce1461b9 chore(sentry): add powershell user auth module connection errors to ignored list (#8420) 2025-08-04 11:58:05 +02:00
Alejandro Bailo 9da5fb67c3 feat(ui): S3 integration (#8391) 2025-08-04 11:43:14 +02:00
Chandrapal BadshahandChandrapal Badshah eb1c1791e4 fix: clear only last message on error (#8431)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-08-04 10:33:45 +02:00
581afd38e6 fix: add default values for S3 class (#8417)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-01 13:50:51 +02:00
sumit-tft 19a735aafe chore(ui): remove misconfigurations from Top Failed Findings in the s… (#8426) 2025-08-01 12:47:17 +02:00
Paul NegeduandMrCloudSec 2170fbb1ab feat(aws): add s3_bucket_shadow_resource_vulnerability check (#8398)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-08-01 18:26:03 +08:00
Pablo Lara 90c6c6b98d feat: add new provider GitHub and update enum source of truth (#8421) 2025-08-01 10:03:47 +02:00
sumit-tft 02b416b4f8 chore(ui): remove browse all resources from the sidebar (#8418) 2025-07-31 16:13:30 +02:00
Hugo Pereira Brito 1022b5e413 chore(docs): add a step to check development guide (#8416) 2025-07-31 12:45:15 +02:00
Pablo Lara d1bad9d9ab chore: rename menu item (#8415) 2025-07-31 12:10:07 +02:00
Rubén De la Torre Vico 178f3850be chore: add M365 provider to PR labeler (#8406) 2025-07-31 17:32:18 +08:00
Adrián Jesús Peña Rodríguez d239d299e2 fix(s3): use enabled to filter (#8409) 2025-07-31 10:00:05 +02:00
Pepe Fagoaga 88fae9ecae chore(ui): remove changelog entry (#8410) 2025-07-31 09:27:11 +02:00
Hugo Pereira Brito a3bff9705c fix(tests): github and iac providers arguments_test naming and structure (#8408) 2025-07-30 17:16:34 +02:00
César Arroba 75989b09d7 chore(gha): fix payload on merged PR action (#8407) 2025-07-30 16:59:40 +02:00
Pablo Lara 9a622f60fe feat(providers): add GitHub provider support with credential types (#8405) 2025-07-30 15:55:40 +02:00
Rubén De la Torre Vico 7cd1966066 fix(azure,m365): use default tenant domain instead of first domain in list (#8402) 2025-07-30 13:23:25 +02:00
Pedro Martín 77e59203ae feat(prowler-threatscore): remove and add requirements (#8401) 2025-07-30 13:09:51 +02:00
0a449c7e13 fix(lighthouse): Display errors in Lighthouse & allow resending message (#8358)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-07-30 12:32:48 +02:00
Adrián Jesús Peña Rodríguez 163fbaff19 feat(integrations): add s3 integration (#8056) 2025-07-30 12:05:46 +02:00
Sergio Garcia 7ec514d9dd feat(aws): new check bedrock_api_key_no_long_term_credentials (#8396) 2025-07-30 17:04:16 +08:00
Hugo Pereira Brito b63f70ac82 fix(m365): enhance execution to avoid multiple error calls (#8353) 2025-07-30 14:54:27 +08:00
2c86b3a990 feat: Add lighthouse banner (#8259)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-07-29 12:30:57 +02:00
Daniel Barranquero 12443f7cbb feat(docs): update m365 and azure docs (#8393) 2025-07-29 11:58:03 +02:00
Rubén De la Torre VicoandDaniel Barranquero 3a8c635b75 docs(dev-guide): add generic best practices for checks and services (#8074)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
2025-07-29 11:04:26 +02:00
8bc6e8b7ab docs(getting-started): improve quality redrive (#7963)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-07-29 11:04:12 +02:00
9ca1899ebf docs(tutorials): improve quality redrive (#7915)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2025-07-29 11:03:52 +02:00
Sergio Garcia 1bdcf2c7f1 refactor(iac): revert importingcheckov as python library (#8385) 2025-07-29 15:55:28 +08:00
Pedro MartínandSergio Garcia 92a804bf88 fix(prowler-threatscore): remove typo from description req 1.2.3 - m365 (#8384)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-28 23:55:38 +08:00
ghkim583 f85ad9a7a2 chore(aws): minor fixes for the kisa isms-p compliance (#8386) 2025-07-28 17:51:20 +02:00
Pedro Martín 308c778bad fix(kisa): change the way of counting the PASS/FAILED reqs (#8382) 2025-07-28 21:56:58 +08:00
Jaen-923andghkim583 ee06d3a68a chore(aws): update kisa-isms-p compliance (#8367)
Co-authored-by: ghkim583 <203069125+ghkim583@users.noreply.github.com>
2025-07-28 21:55:50 +08:00
Andoni Alonso 8dc4bd0be8 feat(github): add repository and organization scoping support (#8329) 2025-07-28 21:43:41 +08:00
Pedro Martín bf9e38dc5c fix(docs): remove typo from getting started - github (#8380) 2025-07-28 20:18:13 +08:00
Aviad LevyandMrCloudSec a85b89ffb5 fix(ec2): add check that protocol is matched in security group checks (#8374)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-28 19:53:08 +08:00
César Arroba 87da11b712 chore(gha): delete repo limitation for bump workflow (#8379) 2025-07-28 13:22:19 +02:00
César Arroba 8b57f178e0 chore(gha): improve e2e pipeline (#8378) 2025-07-28 13:22:12 +02:00
Prowler Botandprowler-bot 7830ed8b9f chore(regions_update): Changes in regions for AWS services (#8376)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-28 17:56:48 +08:00
Kay Agahd d4e66c4a6f chore(sqs): clean up code (#8366) 2025-07-25 20:10:34 +08:00
Rubén De la Torre VicoandSergio Garcia 1cfe610d47 feat(azure/vm): add new check vm_scaleset_not_empty (#8192)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-25 18:42:03 +08:00
Rubén De la Torre VicoandSergio Garcia d9a9236ab7 feat(azure/vm): add new check vm_desired_sku_size (#8191)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-25 17:51:01 +08:00
Hugo Pereira Brito 285aea3458 fix(docs): change Exchange Administrator role to Global Reader for M365 (#8360) 2025-07-25 15:45:30 +08:00
César Arroba b051aeeb64 chore(gha): automate e2e tests with new workflow (#8361) 2025-07-24 16:54:01 +02:00
Pedro Martín b99dce6a43 feat(azure): add CIS 4.0 (#7782) 2025-07-24 22:29:46 +08:00
Andoni AlonsoandMrCloudSec 04749c1da1 fix(aws): sns_topics_not_publicly_accessible false positive with aws:SourceArn conditions (#8340)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-24 18:03:30 +08:00
Chandrapal BadshahandChandrapal Badshah 44d70f8467 fix(lighthouse): update prompt and tool schema for checks tool (#8265)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-07-24 10:50:36 +02:00
Andoni AlonsoandSergio Garcia 95791a9909 chore(aws): replace known errors with warnings (#8347)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-24 15:34:45 +08:00
sumit-tftandalejandrobailo ad0b8a4208 feat(ui): create CustomLink component and refactor links to use it (#8341)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-23 19:10:51 +02:00
5669a42039 fix(wazuh): patch command injection vulnerability in prowler-wrapper.py (#8331)
Co-authored-by: Test User <test@example.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-23 16:06:55 +02:00
Kay AgahdandHugo Pereira Brito 83b328ea92 fix(aws): avoid false positives in SQS encryption check for ephemeral queues (#8330)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-07-23 21:03:02 +08:00
Alejandro Bailo a6c88c0d9e test: timeout updated for E2E (#8351) 2025-07-23 13:11:32 +02:00
Sergio Garcia 922f9d2f91 docs(gcp): update GCP permissions (#8350) 2025-07-23 17:43:42 +08:00
a69d0d16c0 fix(azure/storage): handle when Azure API set values to None (#8325)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-23 17:11:04 +08:00
Alejandro Bailo 676cc44fe2 feat: env keys behavior updated (#8348) 2025-07-23 10:44:28 +02:00
Alejandro BailoandCésar Arroba 3840e40870 test(e2e): Sign-in (#8337)
Co-authored-by: César Arroba <cesar@prowler.com>
2025-07-22 18:04:54 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ab2d57554a chore(deps): bump form-data from 4.0.3 to 4.0.4 in /ui (#8346)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 17:53:32 +02:00
César Arroba cbb5b21e6c chore(gha): e2e tests pipeline with API services (#8338) 2025-07-22 17:49:23 +02:00
Sergio Garcia 1efd5668ce feat(api): add GitHub provider support (#8271) 2025-07-22 23:26:02 +08:00
Sergio Garcia ca86aeb1d7 feat(aws): new check bedrock_api_key_no_administrative_privileges (#8321) 2025-07-22 22:06:17 +08:00
Víctor Fernández Poyatos 4f2a8b71bb feat(performance): resources scenario (#8345) 2025-07-22 13:01:19 +02:00
Prowler Botandprowler-bot 3b0cb3db85 chore(regions_update): Changes in regions for AWS services (#8333)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-22 17:23:24 +08:00
Víctor Fernández Poyatos 00c527ff79 chore: update Prowler changelog for v5.9.2 (#8342) 2025-07-22 10:53:22 +02:00
Víctor Fernández Poyatos ab348d5752 feat(resources): Optimize findings prefetching during resource views (#8336) 2025-07-21 16:33:07 +02:00
Daniel Barranquero dd713351dc fix(defender): avoid duplicated findings in check defender_domain_dkim_enabled (#8334) 2025-07-21 13:07:26 +02:00
sumit-tftandalejandrobailo fa722f1dc7 feat(ui): add 32-character limit validation for scan name in create a… (#8319)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-21 10:00:25 +02:00
Pedro Martín b0cc3978d0 feat(docs): add info about updating Prowler App (#8320) 2025-07-21 07:44:07 +02:00
César Arroba aa843b823c chore(gha): fix action version (#8327) 2025-07-18 15:00:32 +02:00
Víctor Fernández Poyatos 020edc0d1d fix(tasks): calculate failed findings for resources during scan (#8322) 2025-07-18 13:19:22 +02:00
César Arroba 036da81bbd chore(gha): fix api prowler version (#8323) 2025-07-18 12:43:38 +02:00
sumit-tftandalejandrobailo 4428bcb2c0 feat(ui): update step title and description in cloud provider update … (#8303)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-18 10:11:44 +02:00
Prowler Botandprowler-bot 21de9a2f6f chore(release): Bump version to v5.10.0 (#8314)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-17 19:38:28 +02:00
231d933b9e chore(docs): SAML documentation (#8137)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-17 23:22:49 +05:45
Alejandro Bailo 2ad360a7f9 docs(ui): Mutelist documentation (#8201) 2025-07-17 23:15:20 +05:45
Víctor Fernández Poyatos 51b67f00d6 chore: update changelogs for v5.9.0 (#8313) 2025-07-17 17:15:58 +02:00
Pepe Fagoaga ab378684ae chore(ui): remove inventory group label (#8311) 2025-07-17 20:29:52 +05:45
e89df617ef chore(lighthouse): Rename to Lighthouse AI (#8262)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-17 16:25:32 +02:00
Alejandro Bailo 8496a6b045 fix: muted filter removed from url when value is true (#8310) 2025-07-17 19:12:36 +05:45
Pepe Fagoaga 28f3cf363b fix(actions): build API if the SDK changes (#8309) 2025-07-17 14:35:51 +02:00
Pepe Fagoaga eb3d4b25e3 chore: improve info in the download button (#8307) 2025-07-17 16:38:45 +05:45
Rubén De la Torre Vico 1211fe706e feat(azure/defender): add new check defender_attack_path_notifications_properly_configured (#8245) 2025-07-17 12:40:26 +02:00
Pedro MartínandPepe Fagoaga c4a9280ebb fix(m365): handle tenant_id in mutelist (#8306)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-17 12:37:13 +02:00
Alejandro Bailo 0f12fb92ed fix: Middleware redirection to /profile (#8305) 2025-07-17 11:49:24 +02:00
Víctor Fernández Poyatos ee974a6316 feat(tasks): Improve memory usage and performance in overview tasks (#8300) 2025-07-17 10:49:25 +02:00
d004a0c931 feat(ui): Add resources view as inventory (#7760)
Co-authored-by: sumit_chaturvedi <chaturvedi.sumit@tftus.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-17 10:01:05 +02:00
Pepe Fagoaga 087e01cc4f fix(checks_loader): Handle multiple services and severities (#8302) 2025-07-17 13:39:29 +05:45
74940e1fc4 fix(check_metadata): add service name validator (#8289)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-07-17 13:10:30 +05:45
Hugo Pereira Brito 19e35bf9a8 feat: add new check entra_intune_enrollment_sign_in_frequency_every_time (#8223) 2025-07-16 17:13:50 +02:00
César Arroba 7213187e6c chore(gha): add target_commitish to the release creation step (#8297) 2025-07-16 16:07:06 +02:00
Pedro Martín 4b104e92f0 chore(prowler-threatscore): improve the way of calculating the score (#8264) 2025-07-16 15:26:44 +02:00
Hugo Pereira Brito 7179119b0e fix(outputs): identity type and id for prowler cloud (#8243) 2025-07-16 15:23:46 +02:00
César Arroba cf2738810a chore(gha): prowler release preparation workflow (#8268) 2025-07-16 13:45:34 +02:00
Samuele Pasini 389216570a fix: typo documentdb service name (#8293) 2025-07-16 18:39:19 +08:00
Alejandro Bailo 2becf45f33 feat: Next.js middleware improved (#8295) 2025-07-16 12:39:05 +02:00
Sergio Garcia c32ce7eb97 fix(azure): use Subscription ID for mutelist (#8290) 2025-07-16 18:34:38 +08:00
sumit-tftandAlejandro Bailo 94e66a91a6 feat(ui): add link in Scans view to navigate to Compliance overview (#8251)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2025-07-16 12:34:21 +02:00
sumit-tftandAlejandro Bailo 1ac4417f74 feat(ui): add status column to findings table in compliance detail view (#8244)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
2025-07-16 12:23:40 +02:00
Hugo Pereira Brito 57c5f7c12d fix(networkfirewall): ServiceName in checks metadata (#8291) 2025-07-16 16:59:42 +08:00
Alejandro Bailo 19203f92b3 feat: menu label hidden in side-menu if no items (#8292) 2025-07-16 14:37:56 +05:45
Alejandro Bailo c5b1bf3e52 feat: allow to restrict routes based on permissions (#8287) 2025-07-16 14:21:45 +05:45
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f845176494 chore(deps): bump aiohttp from 3.11.18 to 3.12.14 in /api (#8276)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 10:21:06 +02:00
Daniel Barranquero f0ed866946 fix(entra): entra_users_mfa_capable check report (#8288) 2025-07-16 16:06:36 +08:00
Alejandro BailoandPepe Fagoaga 834a7d3b69 fix(scans): capture 403 when no permissions (#8280)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-15 21:43:50 +05:45
Pepe Fagoaga 24a50c6ac2 fix(schedules): returns 409 on conflict (#8258) 2025-07-15 21:29:05 +05:45
Víctor Fernández Poyatos ec8afd773f fix(overviews): apply RBAC to providers overview (#8277) 2025-07-15 17:31:25 +02:00
Rubén De la Torre Vico a09be4c0ba chore(azure/defender): get security contacts from API REST (#8241) 2025-07-15 16:37:43 +02:00
Andoni AlonsoandMrCloudSec 4b62fdcf53 feat(iac): add support for remote repos (#8193)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-15 22:08:27 +08:00
Kay AgahdandMrCloudSec bf0013dae3 fix(aws): make is_service_role more restrictive to avoid false positives (#8274)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-15 22:02:09 +08:00
Hugo Pereira BritoandAndoni A. c82cd5288c feat(docs): add new docker pull issues section (#7972)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-07-15 16:00:08 +02:00
Sergio Garcia ad31a6b3f5 chore(hadolint): run only when necessary (#8284) 2025-07-15 21:53:05 +08:00
Rubén De la Torre Vico 20c7c9f8de fix(dashboard): count rows in the CSV more efficiently (#8257) 2025-07-15 15:40:36 +02:00
Daniel Barranquero 0cfe41e452 fix(dynamodb): update broken link in dynamodb_tables_kms_cmk_encryption_enabled (#8273) 2025-07-15 14:54:14 +02:00
Hugo Pereira BritoandAndoni A. 1b254feadc feat(docs): add getting-started page (#8275)
Co-authored-by: Andoni A. <14891798+andoniaf@users.noreply.github.com>
2025-07-15 14:11:20 +02:00
Matt KeelerandSergio Garcia 15954d8a01 fix: reword iam_user_accesskey_unused title & description (#8233)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-15 20:02:34 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ff122c9779 chore(deps): bump aiohttp from 3.12.13 to 3.12.14 (#8278)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 19:45:03 +08:00
Daniel Barranquero a012397e55 fix(dashboard): security posture showing incomplete data (#8270) 2025-07-15 13:19:55 +02:00
Sergio Garcia 7da6d7b5dd chore(github): add test_connection function (#8248) 2025-07-15 17:01:40 +08:00
Víctor Fernández Poyatos db6a27d1f5 feat(resources): latest and metadata endpoints and performance (#8112) 2025-07-14 18:02:06 +02:00
Alejandro BailoandPepe Fagoaga e07c833cab feat: SAML toast error (#8267)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-14 17:04:23 +02:00
Adrián Jesús Peña Rodríguez 728fc9d6ff fix(saml): remove user in case of error (#8260) 2025-07-14 14:07:27 +02:00
Prowler Botandprowler-bot cf9ff78605 chore(regions_update): Changes in regions for AWS services (#8263)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-14 19:45:38 +08:00
Adrián Jesús Peña Rodríguez a2faf548af chore: update changelog (#8255) 2025-07-11 12:06:03 +02:00
Adrián Jesús Peña Rodríguez 8bcec4926b fix: set lxml version (#8253) 2025-07-11 11:43:31 +02:00
Hugo Pereira Brito a4e96f809b fix(docs): GitHub provider mkdocs and -h (#8246) 2025-07-11 16:32:15 +08:00
Adrián Jesús Peña Rodríguez fa27255dd7 chore(saml): redirect to login page on fail (#8247) 2025-07-11 09:22:38 +02:00
Pepe Fagoaga 05360e469f chore(bump): add no-changelog label (#8240) 2025-07-10 19:14:37 +08:00
Hugo Pereira Brito 9d405ddcbd fix: changelog entries with new specification (#8232) 2025-07-10 14:40:33 +05:45
Víctor Fernández Poyatos 430f831543 feat(exceptions): add custom error for provider connection during scans (#8234) 2025-07-10 14:13:19 +05:45
Pepe Fagoaga da9d7199b7 chore(changelog): add missing entry from the password policy (#8236) 2025-07-10 09:07:04 +02:00
Pepe Fagoagaandalejandrobailo d63a383ec6 feat(security): password strength (#8225)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
2025-07-10 11:50:22 +05:45
Víctor Fernández Poyatos 55c226029e feat(resources): optimize include parameters for resources view (#8229) 2025-07-09 16:16:56 +02:00
Alejandro Bailo 8d2f6aa30c feat: Include/exclude muted findings (#8228) 2025-07-09 16:06:05 +02:00
Rubén De la Torre VicoandSergio Garcia a319f80701 feat(storage): add new check storage_smb_protocol_version_is_latest (#8128)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-09 17:28:00 +08:00
Adrián Jesús Peña Rodríguez 15a8671f0d feat(saml): prevent duplicate SAML entityID configuration (#8224) 2025-07-09 09:50:22 +02:00
Rubén De la Torre Vico d34e709d91 fix(azure/storage): use BaseModel for all Storage models (#8222) 2025-07-09 15:49:17 +08:00
Hugo Pereira Brito ddc53c3c6d fix(firehose): list all streams and fix firehose_stream_encrypted_at_rest logic (#8213) 2025-07-09 15:38:54 +08:00
a3aef18cfe feat: Mutelist implementation (#8190)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
Co-authored-by: Drew Kerrigan <drew@prowler.com>
2025-07-09 08:15:23 +02:00
Alejandro Bailo 49ca3ca325 fix: SAML 403 message (#8221) 2025-07-09 08:10:14 +02:00
89c67079a3 feat: Processors API endpoint, implement MuteList (#7993)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-08 21:33:28 +05:45
Pepe Fagoaga 2de8075d87 fix(overview): use findings latest to get new (#8219) 2025-07-08 15:48:19 +02:00
Adrián Jesús Peña Rodríguez e124275dbf fix(saml): ensure SocialApp and SAMLDomainIndex are deleted with SAMLConfiguration (#8210) 2025-07-08 13:57:23 +02:00
Rubén De la Torre Vico 760d28e752 chore(deps): update dash libs (#8215) 2025-07-08 19:55:50 +08:00
Víctor Fernández Poyatos 3fb0733887 feat(tasks): create overview queue for summaries and overviews (#8214) 2025-07-08 13:53:23 +02:00
Pablo LaraandPepe Fagoaga 7de9a37edb fix(api): make invitation email comparison case-insensitive (#8206)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-08 16:39:27 +05:45
Pepe FagoagaandVíctor Fernández Poyatos fe00b788cc fix: Remove type validation while updating provider credentials (#8197)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-07-08 15:27:02 +05:45
Rubén De la Torre VicoandSergio Garcia 4c50f4d811 feat(azure/vm): add new check vm_backup_enabled (#8182)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-08 17:01:22 +08:00
Rubén De la Torre Vico c0c736bffe chore: ignore some files from AI editors (#8209) 2025-07-08 10:43:38 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> a3aa7d0a63 chore(deps): bump python from 3.12.10-slim-bookworm to 3.12.11-slim-bookworm (#8157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 16:43:13 +08:00
Rubén De la Torre Vico 3ceb86c4d9 feat(azure/vm): add new check vm_scaleset_associated_load_balancer (#8181) 2025-07-08 16:40:43 +08:00
Rubén De la Torre Vico 3628e7b3e8 feat(azure/vm): add new check vm_ensure_using_approved_images (#8168) 2025-07-08 16:40:33 +08:00
f29c2ac9f0 docs(lighthouse): Add Lighthouse Docs (#8196)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-08 11:56:23 +05:45
Pablo Lara b4927c3ad1 chore: Update CHANGELOG UI (#8204) 2025-07-07 17:54:44 +02:00
Adrián Jesús Peña Rodríguez 19f3c1d310 chore(saml): restore SAML button (#8203) 2025-07-07 17:34:05 +02:00
Adrián Jesús Peña Rodríguez cd97e57521 fix(saml): restore SAML, deactivate urls, enable idp-initiate (#8175) 2025-07-07 16:42:11 +02:00
Hugo Pereira BritoandSergio Garcia b38207507a chore(docs): enhance M365 auth documentation (#8199)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-07 22:01:41 +08:00
Rubén De la Torre Vico ab96e0aac0 feat(azure/vm): add new check vm_linux_enforce_ssh_authentication (#8149) 2025-07-07 22:01:11 +08:00
Prowler Botandprowler-bot 4477cecc59 chore(regions_update): Changes in regions for AWS services (#8198)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-07 18:04:49 +08:00
Pablo Lara 641d671312 chore: upgrade to Next.js 14.2.30 and lock TypeScript to 5.5.4 for ES… (#8189) 2025-07-04 13:20:30 +02:00
Víctor Fernández Poyatos e7c2fa0699 fix(findings): avoid backfill on empty scans (#8183) 2025-07-04 12:24:49 +02:00
Pedro Martín 7eb08b0f14 fix(ec2): allow empty values for http_endpoint in templates (#8184) 2025-07-04 18:03:51 +08:00
Rubén De la Torre VicoandSergio Garcia 6f3112f754 feat(storage): add new check storage_smb_channel_encryption_with_secure_algorithm (#8123)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-04 15:26:33 +08:00
Kay AgahdandSergio Garcia f5ecae6da1 fix(iam): detect wildcarded ARNs in sts:AssumeRole policy resources (#8164)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-03 23:09:48 +08:00
Prowler Botandprowler-bot 1c75f6b804 chore(release): Bump version to v5.9.0 (#8178)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-07-03 23:08:37 +08:00
Daniel BarranqueroandSergio Garcia 91b64d8572 chore(docs): update m365 docs for app auth in cloud (#8147)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-07-03 23:08:15 +08:00
Pablo Lara 233ae74560 fix: disable dynamic filters for now (#8177) 2025-07-03 14:17:02 +02:00
Alejandro Bailo fac97f9785 fix: remove duplicated calls during promise all resolving (#8176) 2025-07-03 14:02:57 +02:00
Pablo Lara e81c7a3893 fix: bug when updating credentials for m365 (#8173) 2025-07-03 11:31:40 +02:00
Adrián Jesús Peña RodríguezandVíctor Fernández Poyatos d6f26df2e8 refactor(migrations): remove saml migrations (#8167)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-07-02 17:23:08 +02:00
Sergio Garcia ece74e15fd chore(sdk): update changelog (#8166) 2025-07-02 16:11:48 +02:00
sumit-tftandPablo Lara eea6d07259 chore(ui): update capitalization of Sign In and Sign Up to match UI s… (#8136)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-07-02 16:01:29 +02:00
Víctor Fernández Poyatos 4a6d7a5be2 chore: bump API changelog to v5.8.0 (#8165) 2025-07-02 16:00:43 +02:00
Alejandro Bailo 883c5d4e56 feat: client side validation (#8161) 2025-07-02 15:43:20 +02:00
Adrián Jesús Peña Rodríguez f1f998c2fa chore: update spec (#8162) 2025-07-02 13:19:57 +02:00
Adrián Jesús Peña Rodríguez 5276e38f1d chore: disable SAML endpoints (#8160) 2025-07-02 12:51:57 +02:00
Pablo Lara ad98a4747f chore: Hide all SAML config for v5.8 (#8159) 2025-07-02 12:46:04 +02:00
Alejandro Bailo 5798321dc6 feat: saml e2e improvements (#8158) 2025-07-02 11:57:56 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bf58728d29 chore(deps-dev): bump brace-expansion from 1.1.11 to 1.1.12 in /ui (#8003)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 11:25:17 +02:00
Sergio GarciaandRubén De la Torre Vico fcea3b6570 docs(iac): add documentation for IaC (#8150)
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2025-07-02 17:20:34 +08:00
Neil MillardandMrCloudSec 965111245a feat(aws): add new check for Codebuild projects visibility (#8127)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-07-02 17:20:15 +08:00
Rubén De la Torre Vico f78a29206c fix(azure): use Pydantic models in VM service and fix managed disk logic (#8151) 2025-07-02 16:23:51 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c719d705e0 chore(deps): bump trufflesecurity/trufflehog from 3.88.35 to 3.89.2 (#8156)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 15:36:10 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8948ee6868 chore(deps): bump docker/setup-buildx-action from 3.10.0 to 3.11.1 (#8153)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 15:29:21 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 24fb31e98f chore(deps): bump github/codeql-action from 3.28.18 to 3.29.2 (#8155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 14:24:12 +08:00
Adrián Jesús Peña Rodríguez c8b193e658 fix(saml): add user to SAML tenant (#8152) 2025-07-01 18:41:16 +02:00
Alejandro Bailo 6d27738c4d fix: HotFIX related with ACS SAML url (#8148) 2025-07-01 13:10:46 +02:00
Adrián Jesús Peña Rodríguez 17b7becfdf fix(saml): limit attributes length to satisfy the socialapp restriction (#8145) 2025-07-01 12:03:20 +02:00
Alejandro Bailo cfa7f271d2 fix: Minor changes detected while SAML E2E (#8146) 2025-07-01 11:50:47 +02:00
Pedro Martín e61a97cb65 fix(api): handle ISO27001 - M365 in exports (#8143) 2025-07-01 10:19:56 +02:00
Pablo LaraandPepe Fagoaga cd4a1ad8a7 chore: clarify M365 context due to credential changes (#8144)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-07-01 09:01:17 +02:00
Alejandro Bailo e650d19a30 feat: enhance getScans API to support fields and include parameters; … (#8140) 2025-07-01 08:13:48 +02:00
Pedro Martín f930739a3d fix(ui): remove typo from compliance detailed view (#8142) 2025-06-30 18:03:45 +02:00
Sergio Garcia 89fc698a0e fix(m365): handle none attribute in exchange transport rule (#8141) 2025-06-30 23:13:18 +08:00
Pablo Lara 6acb6bbf8e docs: update changelog (#8139) 2025-06-30 16:34:03 +02:00
971424f822 fix: ACS dynamic url and password input visible in sign up (#8131)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-06-30 16:17:34 +02:00
Adrián Jesús Peña Rodríguez 9ba1ae1ced restore: change api redirect (#8138) 2025-06-30 16:15:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 062db4cc70 chore(deps): bump protobuf from 6.30.2 to 6.31.1 in /api (#8053)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 21:05:10 +08:00
Pepe Fagoaga dc4db10c41 fix(version): only for master branch (#7850) 2025-06-30 16:50:32 +05:45
Rubén De la Torre Vico 68a542ef64 chore(CHANGELOG): put all checks entries in same format (#8134) 2025-06-30 16:50:12 +05:45
32f3787e18 feat(m365powershell): add pwsh authentication via service principal (#7992)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-30 18:42:18 +08:00
Víctor Fernández Poyatos 6792bea319 fix(compliance): Avoid initializing Prowler provider (#8133) 2025-06-30 12:14:03 +02:00
Prowler Botandprowler-bot ae4b43c137 chore(regions_update): Changes in regions for AWS services (#8132)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-06-30 17:53:21 +08:00
Rubén De la Torre Vico d576c4f1c4 docs(developer-guide): add configurable checks documentation (#8122) 2025-06-30 16:47:27 +08:00
Pablo Lara ddc0596aa2 chore: tweaks for SAML config in profile page (#8130) 2025-06-30 09:40:02 +02:00
Rubén De la Torre Vico 636bdb6d0a docs(prowler-app): add new auth method for GCP (#8129) 2025-06-30 15:21:03 +08:00
Alejandro BailoandAdrián Jesús Peña Rodríguez 4a839b0146 feat: update SAML login URL handling and redirect logic (#8095)
Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
2025-06-27 14:44:04 +02:00
Pablo Lara 73e244dce5 docs: update changelog (#8125) 2025-06-27 13:51:56 +02:00
Adrián Jesús Peña Rodríguez d8ed70236b refactor(s3): adapt test_connection to match AwsProvider (#8088) 2025-06-27 13:23:59 +02:00
Sergio GarciaandPedro Martín bcc96ab4f2 fix(gcp): handle case sensitivity in block-project-ssh-keys (#8115)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-06-27 19:03:51 +08:00
fd53a8c9d0 feat: Playright setup (#8107)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
2025-06-27 11:47:21 +02:00
Daniel Barranquero 7b58d1dd56 fix: checks with no resource name (#8120) 2025-06-27 17:40:43 +08:00
Víctor Fernández Poyatos 7858c147f7 fix(spec): API specification (#8119) 2025-06-27 10:49:36 +02:00
Alejandro Bailo 8e635b3bd4 feat: saml sso ui integration (#8094) 2025-06-27 10:45:21 +02:00
Pedro Martín 2e97e37316 feat(dashboard): improve overview page (#8118) 2025-06-27 15:41:48 +08:00
Pedro Martín cd804836a1 docs(dev): add info about installing prowler for a branch (#8116) 2025-06-26 23:00:31 +08:00
Víctor Fernández Poyatos d102ee2fd5 chore: ignore Flask Safety alert in API (#8114) 2025-06-26 16:02:39 +02:00
Pedro Martín 325e5739a2 fix(compliance): handle latest assessment date for each account (#8108) 2025-06-26 17:48:35 +08:00
Sergio Garcia 98da3059b4 refactor(iac): import checkov python library (#8093) 2025-06-25 21:36:21 +08:00
Chandrapal BadshahandChandrapal Badshah 80fd5d1ba6 fix: update lighthouse chat page name (#8106)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-06-25 12:48:20 +02:00
Jack HollowayandMrCloudSec 85242c7909 fix(aws): retrieve correctly ECS Container insights settings (#8097)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-25 15:54:20 +08:00
Daniel BarranqueroandMrCloudSec ea6ab406c8 fix(organizations): Key Error: Statement in check organizations_scp_deny_regions (#8091)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-25 15:23:38 +08:00
Rubén De la Torre VicoandSergio Garcia cbf2a28bac feat(azure): add new check keyvault_access_only_through_private_endpoints (#8072)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-24 22:04:02 +08:00
Adrián Jesús Peña Rodríguez 5b1e7bb7f9 fix(saml): avoid IndexError when some attributes are not specified (#8089) 2025-06-24 15:55:01 +02:00
e108b2caed fix(aws): fix logic in VPC and ELBv2 checks (#8077)
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-24 19:13:54 +08:00
Rubén De la Torre VicoandSergio Garcia df1abb2152 feat(azure): add new check monitor_alert_service_health_exists (#8067)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-24 18:04:20 +08:00
Rubén De la Torre Vico e0465f2aa2 fix(azure): consolidate file share properties to the storage account level (#8087) 2025-06-24 17:37:05 +08:00
Drew Kerrigan 51467767cd fix: allow raising exceptions from validate_mutelist (#8086) 2025-06-24 13:14:46 +05:45
Pablo Lara bc71e7fb3b chore: set filters panel to be always open by default (#8085) 2025-06-23 15:05:53 +02:00
sumit-tftandPablo Lara 6a331c05e8 fix(ui): resolve accessibility warnings for Sheet and SVG elements (#8019)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-06-23 13:25:05 +02:00
César Arroba 7ab503a096 chore(gha): avoid comment on PRs for check-changelog workflow (#8084) 2025-06-23 13:17:03 +02:00
César Arroba b368190c9f chore(gha): avoid comment on PRs for check-changelog workflow (#8083) 2025-06-23 19:13:13 +08:00
Víctor Fernández Poyatos 8915fdff18 fix(scan): set scheduler_task to null when removing periodic tasks (#8082) 2025-06-23 12:53:58 +02:00
Víctor Fernández Poyatos 9bf108e9cc tests(compliance): add performance tests for compliance (#8073) 2025-06-23 12:09:30 +02:00
Prowler Botandprowler-bot 87708e39cf chore(regions_update): Changes in regions for AWS services (#8079)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2025-06-23 17:54:27 +08:00
César Arroba 44927c44e9 chore(gha): add permissions on check-changelog workflow (#8080) 2025-06-23 11:49:48 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 71aa29cf24 chore(deps): bump urllib3 from 1.26.20 to 2.5.0 (#8063)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 17:49:20 +08:00
Víctor Fernández Poyatos aa14daf0db fix(schema): API reference documentation (#8078) 2025-06-23 11:04:25 +02:00
Daniel Barranquero eb5dbab86e feat(docs): update Azure and M365 docs with needed permissions (#8075) 2025-06-23 10:12:11 +02:00
Víctor Fernández Poyatos 223aab8ece chore(API): skip safety vulnerabilities related to asteval (#8076) 2025-06-20 14:28:23 +02:00
César Arroba 3ec57340a0 chore(gha): check changelog when label is added or deleted (#8071) 2025-06-20 16:35:19 +05:45
Pablo Lara 80d73cc05b feat: integrate Google Tag Manager manually to avoid ORB blocking (#8070) 2025-06-20 12:47:17 +02:00
César ArrobaandPepe Fagoaga 94f02df11e chore(gha): check changelog changes on pull request (#7991)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-06-19 14:51:59 +05:45
Pepe Fagoaga c454ceb296 fix(changelog): Add missing entries (#8066) 2025-06-19 14:12:39 +05:45
Pepe Fagoaga 76ec13a1d6 chore(ocsf): remove version number and point to the latest (#8064) 2025-06-19 13:33:28 +05:45
Pepe Fagoaga 783b6ea982 chore(api): clean up old files (#8051) 2025-06-19 11:57:48 +05:45
Alejandro BailoandPablo Lara 6b7b700a98 feat: filters relationships in findings and scans page (#8046)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-06-18 17:19:41 +02:00
César Arroba b3f2a1c532 chore(ui): add NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID variable on Dockerfile (#8061) 2025-06-18 16:31:55 +02:00
Sergio Garcia c4e1bd3ed2 fix: add missing changelog compliance timestamps (#8060) 2025-06-18 16:28:48 +02:00
Sergio Garcia d0d4e0d483 fix(compliance): use unified timestampt for all requirements (#8052) 2025-06-18 22:00:51 +08:00
Pablo Lara 14a9f0e765 feat: add Google Tag Manager integration (#8058) 2025-06-18 15:47:48 +02:00
Rubén De la Torre VicoandMrCloudSec b572575c8d feat(azure): add new check iam_role_user_access_admin_restricted (#8040)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-18 21:24:23 +08:00
Rubén De la Torre VicoandAndoni Alonso a626e41162 docs: add provider-specific developer guide sections (#7996)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2025-06-18 21:20:33 +08:00
Hugo Pereira Brito 22343faa1e feat(storage): add new check storage_default_to_entra_authorization_enabled (#7981) 2025-06-18 21:16:07 +08:00
Hugo Pereira Brito c5b37887ef chore: add pr to changelog (#8054) 2025-06-18 14:32:21 +02:00
Rubén De la Torre Vico f9aed36d0b feat(azure): add new check databricks_workspace_cmk_encryption_enabled (#8017) 2025-06-18 18:36:37 +08:00
Hugo Pereira BritoandSergio Garcia facc0627d7 feat(azure): add new check storage_geo_redundant_enabled (#7980)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-18 18:10:02 +08:00
Rubén De la Torre VicoandMrCloudSec 76f0d890e9 feat(azure): add Databricks service and check for workspace VNet injection (#8008)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-06-18 17:38:09 +08:00
Hugo Pereira Brito 7de7122c3b fix(m365): avoid user requests in setup_identity app context and user auth log enhancement (#8043) 2025-06-18 11:27:11 +02:00
Hugo Pereira BritoandSergio Garcia 1b73ab2fe4 feat(storage): add new check storage_cross_tenant_replication_disabled (#7977)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-18 15:54:13 +08:00
Rubén De la Torre VicoandSergio Garcia cc8f6131e6 feat(azure): add new check storage_blob_versioning_is_enabled (#7927)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-18 15:46:38 +08:00
Andoni AlonsoandSergio Garcia dfd5c9aee7 feat(aws): add check to ensure Codebuild Github projects are only use allowed Github orgs (#7595)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-18 00:17:18 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3986bf3f42 chore(deps): bump asteval from 1.0.5 to 1.0.6 (#8049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 00:11:18 +08:00
Sergio Garcia c45ef1e286 chore(deps): update requests dependency (#8048) 2025-06-18 00:04:09 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8d8f498dc2 chore(deps): bump asteval from 1.0.5 to 1.0.6 (#8047)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 23:32:13 +08:00
Sergio GarciaandAndoni Alonso c4bd9122d4 feat(IaC): PoC for IaC Security Scanner (#7852)
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
2025-06-17 23:23:25 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 644cdc81b9 chore(deps): bump requests from 2.32.3 to 2.32.4 in /api (#7986)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 16:46:29 +02:00
Pablo Lara e5584f21b3 feat: make user and password fields optional but mutually required fo… (#8044) 2025-06-17 14:46:00 +02:00
Rubén De la Torre Vico b868d39bef chore(deps): add pre-commit as a dev dependency (#8042) 2025-06-17 18:54:32 +08:00
Alejandro Bailo ef9809f61f fix: correct parenthesis around the render condition (#8041) 2025-06-17 12:22:17 +02:00
Alejandro BailoandPablo Lara 9a04ca3611 feat: touching up compliances views (#8022)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-06-17 11:23:14 +02:00
Pedro MartínandSergio Garcia 1c9b3a1394 feat(m365): add ISO 27001 2022 compliance framework (#7985)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-06-17 17:04:36 +08:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 5ee7bd6459 chore(deps): bump protobuf from 6.30.2 to 6.31.1 (#8037)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 16:31:04 +08:00
Chandrapal BadshahandChandrapal Badshah 05d2b86ba8 feat(lighthouse): update NextJS logic to work with latest APIs (#8033)
Co-authored-by: Chandrapal Badshah <12944530+Chan9390@users.noreply.github.com>
2025-06-17 10:25:37 +02:00
Andoni Alonso 84c30af6f8 chore(sentry): handle exceptions ignores not based in ClassNames (#8034) 2025-06-17 09:42:24 +02:00
e8a829b75e docs(dev-guide): improve quality redrive (#7718)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: Andoni Alonso <14891798+andoniaf@users.noreply.github.com>
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2025-06-17 09:28:22 +02:00
1711 changed files with 301058 additions and 33079 deletions
+17 -40
View File
@@ -6,11 +6,17 @@
PROWLER_UI_VERSION="stable"
AUTH_URL=http://localhost:3000
API_BASE_URL=http://prowler-api:8080/api/v1
NEXT_PUBLIC_API_BASE_URL=${API_BASE_URL}
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
AUTH_TRUST_HOST=true
UI_PORT=3000
# Temp URL for feeds need to use actual
RSS_FEED_URL=https://prowler.com/blog/rss
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
# Google Tag Manager ID
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=""
#### Prowler API Configuration ####
PROWLER_API_VERSION="stable"
@@ -23,6 +29,12 @@ POSTGRES_ADMIN_PASSWORD=postgres
POSTGRES_USER=prowler
POSTGRES_PASSWORD=postgres
POSTGRES_DB=prowler_db
# Read replica settings (optional)
# POSTGRES_REPLICA_HOST=postgres-db
# POSTGRES_REPLICA_PORT=5432
# POSTGRES_REPLICA_USER=prowler
# POSTGRES_REPLICA_PASSWORD=postgres
# POSTGRES_REPLICA_DB=prowler_db
# Celery-Prowler task settings
TASK_RETRY_DELAY_SECONDS=0.1
@@ -79,55 +91,21 @@ DJANGO_CACHE_MAX_AGE=3600
DJANGO_STALE_WHILE_REVALIDATE=60
DJANGO_MANAGE_DB_PARTITIONS=True
# openssl genrsa -out private.pem 2048
DJANGO_TOKEN_SIGNING_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDs4e+kt7SnUJek
6V5r9zMGzXCoU5qnChfPiqu+BgANyawz+MyVZPs6RCRfeo6tlCknPQtOziyXYM2I
7X+qckmuzsjqp8+u+o1mw3VvUuJew5k2SQLPYwsiTzuFNVJEOgRo3hywGiGwS2iv
/5nh2QAl7fq2qLqZEXQa5+/xJlQggS1CYxOJgggvLyra50QZlBvPve/AxKJ/EV/Q
irWTZU5lLNI8sH2iZR05vQeBsxZ0dCnGMT+vGl+cGkqrvzQzKsYbDmabMcfTYhYi
78fpv6A4uharJFHayypYBjE39PwhMyyeycrNXlpm1jpq+03HgmDuDMHydk1tNwuT
nEC7m7iNAgMBAAECggEAA2m48nJcJbn9SVi8bclMwKkWmbJErOnyEGEy2sTK3Of+
NWx9BB0FmqAPNxn0ss8K7cANKOhDD7ZLF9E2MO4/HgfoMKtUzHRbM7MWvtEepldi
nnvcUMEgULD8Dk4HnqiIVjt3BdmGiTv46OpBnRWrkSBV56pUL+7msZmMZTjUZvh2
ZWv0+I3gtDIjo2Zo/FiwDV7CfwRjJarRpYUj/0YyuSA4FuOUYl41WAX1I301FKMH
xo3jiAYi1s7IneJ16OtPpOA34Wg5F6ebm/UO0uNe+iD4kCXKaZmxYQPh5tfB0Qa3
qj1T7GNpFNyvtG7VVdauhkb8iu8X/wl6PCwbg0RCKQKBgQD9HfpnpH0lDlHMRw9K
X7Vby/1fSYy1BQtlXFEIPTN/btJ/asGxLmAVwJ2HAPXWlrfSjVAH7CtVmzN7v8oj
HeIHfeSgoWEu1syvnv2AMaYSo03UjFFlfc/GUxF7DUScRIhcJUPCP8jkAROz9nFv
DByNjUL17Q9r43DmDiRsy0IFqQKBgQDvlJ9Uhl+Sp7gRgKYwa/IG0+I4AduAM+Gz
Dxbm52QrMGMTjaJFLmLHBUZ/ot+pge7tZZGws8YR8ufpyMJbMqPjxhIvRRa/p1Tf
E3TQPW93FMsHUvxAgY3MV5MzXFPhlNAKb+akP/RcXUhetGAuZKLubtDCWa55ZQuL
wj2OS+niRQKBgE7K8zUqNi6/22S8xhy/2GPgB1qPObbsABUofK0U6CAGLo6te+gc
6Jo84IyzFtQbDNQFW2Fr+j1m18rw9AqkdcUhQndiZS9AfG07D+zFB86LeWHt4DS4
ymIRX8Kvaak/iDcu/n3Mf0vCrhB6aetImObTj4GgrwlFvtJOmrYnO8EpAoGAIXXP
Xt25gWD9OyyNiVu6HKwA/zN7NYeJcRmdaDhO7B1A6R0x2Zml4AfjlbXoqOLlvLAf
zd79vcoAC82nH1eOPiSOq51plPDI0LMF8IN0CtyTkn1Lj7LIXA6rF1RAvtOqzppc
SvpHpZK9pcRpXnFdtBE0BMDDtl6fYzCIqlP94UUCgYEAnhXbAQMF7LQifEm34Dx8
BizRMOKcqJGPvbO2+Iyt50O5X6onU2ITzSV1QHtOvAazu+B1aG9pEuBFDQ+ASxEu
L9ruJElkOkb/o45TSF6KCsHd55ReTZ8AqnRjf5R+lyzPqTZCXXb8KTcRvWT4zQa3
VxyT2PnaSqEcexWUy4+UXoQ=
-----END PRIVATE KEY-----"
DJANGO_TOKEN_SIGNING_KEY=""
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7OHvpLe0p1CXpOlea/cz
Bs1wqFOapwoXz4qrvgYADcmsM/jMlWT7OkQkX3qOrZQpJz0LTs4sl2DNiO1/qnJJ
rs7I6qfPrvqNZsN1b1LiXsOZNkkCz2MLIk87hTVSRDoEaN4csBohsEtor/+Z4dkA
Je36tqi6mRF0Gufv8SZUIIEtQmMTiYIILy8q2udEGZQbz73vwMSifxFf0Iq1k2VO
ZSzSPLB9omUdOb0HgbMWdHQpxjE/rxpfnBpKq780MyrGGw5mmzHH02IWIu/H6b+g
OLoWqyRR2ssqWAYxN/T8ITMsnsnKzV5aZtY6avtNx4Jg7gzB8nZNbTcLk5xAu5u4
jQIDAQAB
-----END PUBLIC KEY-----"
DJANGO_TOKEN_VERIFYING_KEY=""
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_SENTRY_DSN=
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.6.0
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.12.2
# Social login credentials
SOCIAL_GOOGLE_OAUTH_CALLBACK_URL="${AUTH_URL}/api/auth/callback/google"
@@ -139,8 +117,7 @@ SOCIAL_GITHUB_OAUTH_CLIENT_ID=""
SOCIAL_GITHUB_OAUTH_CLIENT_SECRET=""
# Single Sign-On (SSO)
SAML_PUBLIC_CERT=""
SAML_PRIVATE_KEY=""
SAML_SSO_CALLBACK_URL="${AUTH_URL}/api/auth/callback/saml"
# Lighthouse tracing
LANGSMITH_TRACING=false
+25
View File
@@ -22,11 +22,26 @@ provider/kubernetes:
- any-glob-to-any-file: "prowler/providers/kubernetes/**"
- any-glob-to-any-file: "tests/providers/kubernetes/**"
provider/m365:
- changed-files:
- any-glob-to-any-file: "prowler/providers/m365/**"
- any-glob-to-any-file: "tests/providers/m365/**"
provider/github:
- changed-files:
- any-glob-to-any-file: "prowler/providers/github/**"
- any-glob-to-any-file: "tests/providers/github/**"
provider/iac:
- changed-files:
- any-glob-to-any-file: "prowler/providers/iac/**"
- any-glob-to-any-file: "tests/providers/iac/**"
provider/mongodbatlas:
- changed-files:
- any-glob-to-any-file: "prowler/providers/mongodbatlas/**"
- any-glob-to-any-file: "tests/providers/mongodbatlas/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"
@@ -42,11 +57,13 @@ mutelist:
- any-glob-to-any-file: "prowler/providers/azure/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/gcp/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/kubernetes/lib/mutelist/**"
- any-glob-to-any-file: "prowler/providers/mongodbatlas/lib/mutelist/**"
- any-glob-to-any-file: "tests/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/aws/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/azure/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/gcp/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/kubernetes/lib/mutelist/**"
- any-glob-to-any-file: "tests/providers/mongodbatlas/lib/mutelist/**"
integration/s3:
- changed-files:
@@ -93,6 +110,10 @@ component/ui:
- changed-files:
- any-glob-to-any-file: "ui/**"
component/mcp-server:
- changed-files:
- any-glob-to-any-file: "mcp_server/**"
compliance:
- changed-files:
- any-glob-to-any-file: "prowler/compliance/**"
@@ -102,3 +123,7 @@ compliance:
review-django-migrations:
- changed-files:
- any-glob-to-any-file: "api/src/backend/api/migrations/**"
metadata-review:
- changed-files:
- any-glob-to-any-file: "**/*.metadata.json"
+4
View File
@@ -8,6 +8,10 @@ If fixes an issue please add it with `Fix #XXXX`
Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
### Steps to review
Please add a detailed description of how to review this PR.
### Checklist
- Are there new checks included in this PR? Yes / No
@@ -6,6 +6,7 @@ on:
- "master"
paths:
- "api/**"
- "prowler/**"
- ".github/workflows/api-build-lint-push-containers.yml"
# Uncomment the code below to test this action on PRs
@@ -61,7 +62,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set short git commit SHA
id: vars
@@ -70,13 +71,13 @@ jobs:
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push container image (latest)
# Comment the following line for testing
@@ -106,7 +107,7 @@ jobs:
- name: Trigger deployment
if: github.event_name == 'push'
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.CLOUD_DISPATCH }}
+3 -3
View File
@@ -44,16 +44,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/api-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: "/language:${{matrix.language}}"
+34 -24
View File
@@ -13,6 +13,7 @@ on:
- "master"
- "v5.*"
paths:
- ".github/workflows/api-pull-request.yml"
- "api/**"
env:
@@ -75,18 +76,20 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: api/**
files: |
api/**
.github/workflows/api-pull-request.yml
files_ignore: ${{ env.IGNORE_FILES }}
- name: Replace @master with current branch in pyproject.toml
- name: Replace @master with current branch in pyproject.toml - Only for pull requests to `master`
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'pull_request' && github.base_ref == 'master'
run: |
BRANCH_NAME="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
echo "Using branch: $BRANCH_NAME"
@@ -99,7 +102,24 @@ jobs:
python -m pip install --upgrade pip
pipx install poetry==2.1.1
- name: Update poetry.lock after the branch name change
- name: Update SDK's poetry.lock resolved_reference to latest commit - Only for push events to `master`
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master'
run: |
# Get the latest commit hash from the prowler-cloud/prowler repository
LATEST_COMMIT=$(curl -s "https://api.github.com/repos/prowler-cloud/prowler/commits/master" | jq -r '.sha')
echo "Latest commit hash: $LATEST_COMMIT"
# Update the resolved_reference specifically for prowler-cloud/prowler repository
sed -i '/url = "https:\/\/github\.com\/prowler-cloud\/prowler\.git"/,/resolved_reference = / {
s/resolved_reference = "[a-f0-9]\{40\}"/resolved_reference = "'"$LATEST_COMMIT"'"/
}' poetry.lock
# Verify the change was made
echo "Updated resolved_reference:"
grep -A2 -B2 "resolved_reference" poetry.lock
- name: Update poetry.lock
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
@@ -107,17 +127,11 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install system dependencies for xmlsec
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
sudo apt-get update
sudo apt-get install -y libxml2-dev libxmlsec1-dev libxmlsec1-openssl pkg-config
- name: Install dependencies
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -136,12 +150,6 @@ jobs:
run: |
poetry check --lock
- name: Prevents known compatibility error between lxml and libxml2/libxmlsec versions - https://github.com/xmlsec/python-xmlsec/issues/320
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run pip install --force-reinstall --no-binary lxml lxml
- name: Lint with ruff
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -169,8 +177,10 @@ jobs:
- name: Safety
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
# 76352, 76353, 77323 come from SDK, but they cannot upgrade it yet. It does not affect API
# TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X
run: |
poetry run safety check --ignore 70612,66963,74429
poetry run safety check --ignore 70612,66963,74429,76352,76353,77323,77744,77745
- name: Vulture
working-directory: ./api
@@ -192,7 +202,7 @@ jobs:
- name: Upload coverage reports to Codecov
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -200,17 +210,17 @@ jobs:
test-container-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: api/**
files_ignore: ${{ env.IGNORE_FILES }}
- name: Set up Docker Buildx
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build Container
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
@@ -7,6 +7,7 @@ on:
- 'v3'
paths:
- 'docs/**'
- '.github/workflows/build-documentation-on-pr.yml'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
@@ -16,9 +17,20 @@ jobs:
name: Documentation Link
runs-on: ubuntu-latest
steps:
- name: Leave PR comment with the Prowler Documentation URI
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
- name: Find existing documentation comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ env.PR_NUMBER }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- prowler-docs-link -->'
- name: Create or update PR comment with the Prowler Documentation URI
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ env.PR_NUMBER }}
body: |
<!-- prowler-docs-link -->
You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
edit-mode: replace
+2 -1
View File
@@ -20,4 +20,5 @@ jobs:
id: conventional-commit-check
uses: agenthunt/conventional-commit-checker-action@9e552d650d0e205553ec7792d447929fc78e012b # v2.0.0
with:
pr-title-regex: '^([^\s(]+)(?:\(([^)]+)\))?: (.+)'
pr-title-regex: '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\([^)]+\))?!?: .+'
+1 -1
View File
@@ -1,4 +1,4 @@
name: Create Backport Label
name: Prowler - Create Backport Label
on:
release:
+2 -2
View File
@@ -7,11 +7,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@90694bf9af66e7536abc5824e7a87246dbf933cb # v3.88.35
uses: trufflesecurity/trufflehog@466da5b0bb161144f6afca9afe5d57975828c410 # v3.90.8
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
@@ -0,0 +1,90 @@
name: MCP Server - Build and Push containers
on:
push:
branches:
- "master"
paths:
- "mcp_server/**"
- ".github/workflows/mcp-server-build-push-containers.yml"
# Uncomment the below code to test this action on PRs
# pull_request:
# branches:
# - "master"
# paths:
# - "mcp_server/**"
# - ".github/workflows/mcp-server-build-push-containers.yml"
release:
types: [published]
env:
# Tags
LATEST_TAG: latest
WORKING_DIRECTORY: ./mcp_server
# Container Registries
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-mcp
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
container-build-push:
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIRECTORY }}
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set short git commit SHA
id: vars
run: |
shortSha=$(git rev-parse --short ${{ github.sha }})
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push container image (latest)
# Comment the following line for testing
if: github.event_name == 'push'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ${{ env.WORKING_DIRECTORY }}
# Set push: false for testing
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.SHORT_SHA }}
cache-from: type=gha
cache-to: type=gha,mode=max
+175
View File
@@ -0,0 +1,175 @@
name: Prowler - PR Conflict Checker
on:
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- "master"
- "v5.*"
# Leaving this commented until we find a way to run it for forks but in Prowler's context
# pull_request_target:
# types:
# - opened
# - synchronize
# - reopened
# branches:
# - "master"
# - "v5.*"
jobs:
conflict-checker:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
**
- name: Check for conflict markers
id: conflict-check
run: |
echo "Checking for conflict markers in changed files..."
CONFLICT_FILES=""
HAS_CONFLICTS=false
# Check each changed file for conflict markers
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
if [ -f "$file" ]; then
echo "Checking file: $file"
# Look for conflict markers
if grep -l "^<<<<<<<\|^=======\|^>>>>>>>" "$file" 2>/dev/null; then
echo "Conflict markers found in: $file"
CONFLICT_FILES="$CONFLICT_FILES$file "
HAS_CONFLICTS=true
fi
fi
done
if [ "$HAS_CONFLICTS" = true ]; then
echo "has_conflicts=true" >> $GITHUB_OUTPUT
echo "conflict_files=$CONFLICT_FILES" >> $GITHUB_OUTPUT
echo "Conflict markers detected in files: $CONFLICT_FILES"
else
echo "has_conflicts=false" >> $GITHUB_OUTPUT
echo "No conflict markers found in changed files"
fi
- name: Add conflict label
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const hasConflictLabel = labels.some(label => label.name === 'has-conflicts');
if (!hasConflictLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['has-conflicts']
});
console.log('Added has-conflicts label');
} else {
console.log('has-conflicts label already exists');
}
- name: Remove conflict label
if: steps.conflict-check.outputs.has_conflicts == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'has-conflicts'
});
console.log('Removed has-conflicts label');
} catch (error) {
if (error.status === 404) {
console.log('has-conflicts label was not present');
} else {
throw error;
}
}
- name: Find existing conflict comment
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
- name: Create or update conflict comment
if: steps.conflict-check.outputs.has_conflicts == 'true'
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
⚠️ **Conflict Markers Detected**
This pull request contains unresolved conflict markers in the following files:
```
${{ steps.conflict-check.outputs.conflict_files }}
```
Please resolve these conflicts by:
1. Locating the conflict markers: `<<<<<<<`, `=======`, and `>>>>>>>`
2. Manually editing the files to resolve the conflicts
3. Removing all conflict markers
4. Committing and pushing the changes
- name: Find existing conflict comment when resolved
if: steps.conflict-check.outputs.has_conflicts == 'false'
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-resolved-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-regex: '(⚠️ \*\*Conflict Markers Detected\*\*|✅ \*\*Conflict Markers Resolved\*\*)'
- name: Update comment when conflicts resolved
if: steps.conflict-check.outputs.has_conflicts == 'false' && steps.find-resolved-comment.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
comment-id: ${{ steps.find-resolved-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
✅ **Conflict Markers Resolved**
All conflict markers have been successfully resolved in this pull request.
- name: Fail workflow if conflicts detected
if: steps.conflict-check.outputs.has_conflicts == 'true'
run: |
echo "::error::Workflow failed due to conflict markers in files: ${{ steps.conflict-check.outputs.conflict_files }}"
exit 1
@@ -0,0 +1,370 @@
name: Prowler - Release Preparation
run-name: Prowler Release Preparation for ${{ inputs.prowler_version }}
on:
workflow_dispatch:
inputs:
prowler_version:
description: 'Prowler version to release (e.g., 5.9.0)'
required: true
type: string
env:
PROWLER_VERSION: ${{ github.event.inputs.prowler_version }}
jobs:
prepare-release:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.12'
- name: Install Poetry
run: |
python3 -m pip install --user poetry
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Configure Git
run: |
git config --global user.name "prowler-bot"
git config --global user.email "179230569+prowler-bot@users.noreply.github.com"
- name: Parse version and determine branch
run: |
# Validate version format (reusing pattern from sdk-bump-version.yml)
if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR_VERSION=${BASH_REMATCH[1]}
MINOR_VERSION=${BASH_REMATCH[2]}
PATCH_VERSION=${BASH_REMATCH[3]}
# Export version components to environment
echo "MAJOR_VERSION=${MAJOR_VERSION}" >> "${GITHUB_ENV}"
echo "MINOR_VERSION=${MINOR_VERSION}" >> "${GITHUB_ENV}"
echo "PATCH_VERSION=${PATCH_VERSION}" >> "${GITHUB_ENV}"
# Determine branch name (format: v5.9)
BRANCH_NAME="v${MAJOR_VERSION}.${MINOR_VERSION}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_ENV}"
# Function to extract the latest version from changelog
extract_latest_version() {
local changelog_file="$1"
if [ -f "$changelog_file" ]; then
# Extract the first version entry (most recent) from changelog
# Format: ## [version] (1.2.3) or ## [vversion] (v1.2.3)
local version=$(grep -m 1 '^## \[' "$changelog_file" | sed 's/^## \[\(.*\)\].*/\1/' | sed 's/^v//' | tr -d '[:space:]')
echo "$version"
else
echo ""
fi
}
# Read actual versions from changelogs (source of truth)
UI_VERSION=$(extract_latest_version "ui/CHANGELOG.md")
API_VERSION=$(extract_latest_version "api/CHANGELOG.md")
SDK_VERSION=$(extract_latest_version "prowler/CHANGELOG.md")
echo "UI_VERSION=${UI_VERSION}" >> "${GITHUB_ENV}"
echo "API_VERSION=${API_VERSION}" >> "${GITHUB_ENV}"
echo "SDK_VERSION=${SDK_VERSION}" >> "${GITHUB_ENV}"
if [ -n "$UI_VERSION" ]; then
echo "Read UI version from changelog: $UI_VERSION"
else
echo "Warning: No UI version found in ui/CHANGELOG.md"
fi
if [ -n "$API_VERSION" ]; then
echo "Read API version from changelog: $API_VERSION"
else
echo "Warning: No API version found in api/CHANGELOG.md"
fi
if [ -n "$SDK_VERSION" ]; then
echo "Read SDK version from changelog: $SDK_VERSION"
else
echo "Warning: No SDK version found in prowler/CHANGELOG.md"
fi
echo "Prowler version: $PROWLER_VERSION"
echo "Branch name: $BRANCH_NAME"
echo "UI version: $UI_VERSION"
echo "API version: $API_VERSION"
echo "SDK version: $SDK_VERSION"
echo "Is minor release: $([ $PATCH_VERSION -eq 0 ] && echo 'true' || echo 'false')"
else
echo "Invalid version syntax: '$PROWLER_VERSION' (must be N.N.N)" >&2
exit 1
fi
- name: Extract changelog entries
run: |
set -e
# Function to extract changelog for a specific version
extract_changelog() {
local file="$1"
local version="$2"
local output_file="$3"
if [ ! -f "$file" ]; then
echo "Warning: $file not found, skipping..."
touch "$output_file"
return
fi
# Extract changelog section for this version
awk -v version="$version" '
/^## \[v?'"$version"'\]/ { found=1; next }
found && /^## \[v?[0-9]+\.[0-9]+\.[0-9]+\]/ { found=0 }
found && !/^## \[v?'"$version"'\]/ { print }
' "$file" > "$output_file"
# Remove --- separators
sed -i '/^---$/d' "$output_file"
# Remove trailing empty lines
sed -i '/^$/d' "$output_file"
}
# Calculate expected versions for this release
if [[ $PROWLER_VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
EXPECTED_UI_VERSION="1.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}"
EXPECTED_API_VERSION="1.$((${BASH_REMATCH[2]} + 1)).${BASH_REMATCH[3]}"
echo "Expected UI version for this release: $EXPECTED_UI_VERSION"
echo "Expected API version for this release: $EXPECTED_API_VERSION"
fi
# Determine if components have changes for this specific release
# UI has changes if its current version matches what we expect for this release
if [ -n "$UI_VERSION" ] && [ "$UI_VERSION" = "$EXPECTED_UI_VERSION" ]; then
echo "HAS_UI_CHANGES=true" >> $GITHUB_ENV
echo "✓ UI changes detected - version matches expected: $UI_VERSION"
extract_changelog "ui/CHANGELOG.md" "$UI_VERSION" "ui_changelog.md"
else
echo "HAS_UI_CHANGES=false" >> $GITHUB_ENV
echo " No UI changes for this release (current: $UI_VERSION, expected: $EXPECTED_UI_VERSION)"
touch "ui_changelog.md"
fi
# API has changes if its current version matches what we expect for this release
if [ -n "$API_VERSION" ] && [ "$API_VERSION" = "$EXPECTED_API_VERSION" ]; then
echo "HAS_API_CHANGES=true" >> $GITHUB_ENV
echo "✓ API changes detected - version matches expected: $API_VERSION"
extract_changelog "api/CHANGELOG.md" "$API_VERSION" "api_changelog.md"
else
echo "HAS_API_CHANGES=false" >> $GITHUB_ENV
echo " No API changes for this release (current: $API_VERSION, expected: $EXPECTED_API_VERSION)"
touch "api_changelog.md"
fi
# SDK has changes if its current version matches the input version
if [ -n "$SDK_VERSION" ] && [ "$SDK_VERSION" = "$PROWLER_VERSION" ]; then
echo "HAS_SDK_CHANGES=true" >> $GITHUB_ENV
echo "✓ SDK changes detected - version matches input: $SDK_VERSION"
extract_changelog "prowler/CHANGELOG.md" "$PROWLER_VERSION" "prowler_changelog.md"
else
echo "HAS_SDK_CHANGES=false" >> $GITHUB_ENV
echo " No SDK changes for this release (current: $SDK_VERSION, input: $PROWLER_VERSION)"
touch "prowler_changelog.md"
fi
# Combine changelogs in order: UI, API, SDK
> combined_changelog.md
if [ "$HAS_UI_CHANGES" = "true" ] && [ -s "ui_changelog.md" ]; then
echo "## UI" >> combined_changelog.md
echo "" >> combined_changelog.md
cat ui_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
if [ "$HAS_API_CHANGES" = "true" ] && [ -s "api_changelog.md" ]; then
echo "## API" >> combined_changelog.md
echo "" >> combined_changelog.md
cat api_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
if [ "$HAS_SDK_CHANGES" = "true" ] && [ -s "prowler_changelog.md" ]; then
echo "## SDK" >> combined_changelog.md
echo "" >> combined_changelog.md
cat prowler_changelog.md >> combined_changelog.md
echo "" >> combined_changelog.md
fi
echo "Combined changelog preview:"
cat combined_changelog.md
- name: Checkout existing branch for patch release
if: ${{ env.PATCH_VERSION != '0' }}
run: |
echo "Patch release detected, checking out existing branch $BRANCH_NAME..."
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
echo "Branch $BRANCH_NAME exists locally, checking out..."
git checkout "$BRANCH_NAME"
elif git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
echo "Branch $BRANCH_NAME exists remotely, checking out..."
git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME"
else
echo "ERROR: Branch $BRANCH_NAME should exist for patch release $PROWLER_VERSION"
exit 1
fi
- name: Verify version in pyproject.toml
run: |
CURRENT_VERSION=$(grep '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_VERSION" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Version mismatch in pyproject.toml (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_VERSION')"
exit 1
fi
echo "✓ pyproject.toml version: $CURRENT_VERSION"
- name: Verify version in prowler/config/config.py
run: |
CURRENT_VERSION=$(grep '^prowler_version = ' prowler/config/config.py | sed -E 's/prowler_version = "([^"]+)"/\1/' | tr -d '[:space:]')
PROWLER_VERSION_TRIMMED=$(echo "$PROWLER_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_VERSION" != "$PROWLER_VERSION_TRIMMED" ]; then
echo "ERROR: Version mismatch in prowler/config/config.py (expected: '$PROWLER_VERSION_TRIMMED', found: '$CURRENT_VERSION')"
exit 1
fi
echo "✓ prowler/config/config.py version: $CURRENT_VERSION"
- name: Verify version in api/pyproject.toml
if: ${{ env.HAS_API_CHANGES == 'true' }}
run: |
CURRENT_API_VERSION=$(grep '^version = ' api/pyproject.toml | sed -E 's/version = "([^"]+)"/\1/' | tr -d '[:space:]')
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_API_VERSION" != "$API_VERSION_TRIMMED" ]; then
echo "ERROR: API version mismatch in api/pyproject.toml (expected: '$API_VERSION_TRIMMED', found: '$CURRENT_API_VERSION')"
exit 1
fi
echo "✓ api/pyproject.toml version: $CURRENT_API_VERSION"
- name: Verify prowler dependency in api/pyproject.toml
if: ${{ env.PATCH_VERSION != '0' && env.HAS_API_CHANGES == 'true' }}
run: |
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
if [ "$CURRENT_PROWLER_REF" != "$BRANCH_NAME_TRIMMED" ]; then
echo "ERROR: Prowler dependency mismatch in api/pyproject.toml (expected: '$BRANCH_NAME_TRIMMED', found: '$CURRENT_PROWLER_REF')"
exit 1
fi
echo "✓ api/pyproject.toml prowler dependency: $CURRENT_PROWLER_REF"
- name: Verify version in api/src/backend/api/v1/views.py
if: ${{ env.HAS_API_CHANGES == 'true' }}
run: |
CURRENT_API_VERSION=$(grep 'spectacular_settings.VERSION = ' api/src/backend/api/v1/views.py | sed -E 's/.*spectacular_settings.VERSION = "([^"]+)".*/\1/' | tr -d '[:space:]')
API_VERSION_TRIMMED=$(echo "$API_VERSION" | tr -d '[:space:]')
if [ "$CURRENT_API_VERSION" != "$API_VERSION_TRIMMED" ]; then
echo "ERROR: API version mismatch in views.py (expected: '$API_VERSION_TRIMMED', found: '$CURRENT_API_VERSION')"
exit 1
fi
echo "✓ api/src/backend/api/v1/views.py version: $CURRENT_API_VERSION"
- name: Checkout existing release branch for minor release
if: ${{ env.PATCH_VERSION == '0' }}
run: |
echo "Minor release detected (patch = 0), checking out existing branch $BRANCH_NAME..."
if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then
echo "Branch $BRANCH_NAME exists remotely, checking out..."
git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME"
else
echo "ERROR: Branch $BRANCH_NAME should exist for minor release $PROWLER_VERSION. Please create it manually first."
exit 1
fi
- name: Prepare prowler dependency update for minor release
if: ${{ env.PATCH_VERSION == '0' }}
run: |
CURRENT_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
BRANCH_NAME_TRIMMED=$(echo "$BRANCH_NAME" | tr -d '[:space:]')
# Create a temporary branch for the PR from the minor version branch
TEMP_BRANCH="update-api-dependency-$BRANCH_NAME_TRIMMED-$(date +%s)"
echo "TEMP_BRANCH=$TEMP_BRANCH" >> $GITHUB_ENV
# Create temp branch from the current minor version branch
git checkout -b "$TEMP_BRANCH"
# Minor release: update the dependency to use the release branch
echo "Updating prowler dependency from '$CURRENT_PROWLER_REF' to '$BRANCH_NAME_TRIMMED'"
sed -i "s|prowler @ git+https://github.com/prowler-cloud/prowler.git@[^\"]*\"|prowler @ git+https://github.com/prowler-cloud/prowler.git@$BRANCH_NAME_TRIMMED\"|" api/pyproject.toml
# Verify the change was made
UPDATED_PROWLER_REF=$(grep 'prowler @ git+https://github.com/prowler-cloud/prowler.git@' api/pyproject.toml | sed -E 's/.*@([^"]+)".*/\1/' | tr -d '[:space:]')
if [ "$UPDATED_PROWLER_REF" != "$BRANCH_NAME_TRIMMED" ]; then
echo "ERROR: Failed to update prowler dependency in api/pyproject.toml"
exit 1
fi
# Update poetry lock file
echo "Updating poetry.lock file..."
cd api
poetry lock
cd ..
# Commit and push the temporary branch
git add api/pyproject.toml api/poetry.lock
git commit -m "chore(api): update prowler dependency to $BRANCH_NAME_TRIMMED for release $PROWLER_VERSION"
git push origin "$TEMP_BRANCH"
echo "✓ Prepared prowler dependency update to: $UPDATED_PROWLER_REF"
- name: Create Pull Request against release branch
if: ${{ env.PATCH_VERSION == '0' }}
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
branch: ${{ env.TEMP_BRANCH }}
base: ${{ env.BRANCH_NAME }}
title: "chore(api): Update prowler dependency to ${{ env.BRANCH_NAME }} for release ${{ env.PROWLER_VERSION }}"
body: |
### Description
Updates the API prowler dependency for release ${{ env.PROWLER_VERSION }}.
**Changes:**
- Updates `api/pyproject.toml` prowler dependency from `@master` to `@${{ env.BRANCH_NAME }}`
- Updates `api/poetry.lock` file with resolved dependencies
This PR should be merged into the `${{ env.BRANCH_NAME }}` release branch.
### License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
author: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
labels: |
component/api
no-changelog
- name: Create draft release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
with:
tag_name: ${{ env.PROWLER_VERSION }}
name: Prowler ${{ env.PROWLER_VERSION }}
body_path: combined_changelog.md
draft: true
target_commitish: ${{ env.BRANCH_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Clean up temporary files
run: |
rm -f prowler_changelog.md api_changelog.md ui_changelog.md combined_changelog.md
@@ -0,0 +1,77 @@
name: Prowler - Check Changelog
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
jobs:
check-changelog:
if: contains(github.event.pull_request.labels.*.name, 'no-changelog') == false
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
env:
MONITORED_FOLDERS: "api ui prowler dashboard"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Get list of changed files
id: changed_files
run: |
git fetch origin ${{ github.base_ref }}
git diff --name-only origin/${{ github.base_ref }}...HEAD > changed_files.txt
cat changed_files.txt
- name: Check for folder changes and changelog presence
id: check_folders
run: |
missing_changelogs=""
for folder in $MONITORED_FOLDERS; do
if grep -q "^${folder}/" changed_files.txt; then
echo "Detected changes in ${folder}/"
if ! grep -q "^${folder}/CHANGELOG.md$" changed_files.txt; then
echo "No changelog update found for ${folder}/"
missing_changelogs="${missing_changelogs}- \`${folder}\`\n"
fi
fi
done
echo "missing_changelogs<<EOF" >> $GITHUB_OUTPUT
echo -e "${missing_changelogs}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Find existing changelog comment
if: github.event.pull_request.head.repo.full_name == github.repository
id: find_comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad #v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- changelog-check -->'
- name: Update PR comment with changelog status
if: github.event.pull_request.head.repo.full_name == github.repository
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
edit-mode: replace
body: |
<!-- changelog-check -->
${{ steps.check_folders.outputs.missing_changelogs != '' && format('⚠️ **Changes detected in the following folders without a corresponding update to the `CHANGELOG.md`:**
{0}
Please add an entry to the corresponding `CHANGELOG.md` file to maintain a clear history of changes.', steps.check_folders.outputs.missing_changelogs) || '✅ All necessary `CHANGELOG.md` files have been updated. Great job! 🎉' }}
- name: Fail if changelog is missing
if: steps.check_folders.outputs.missing_changelogs != ''
run: |
echo "ERROR: Missing changelog updates in some folders."
exit 1
+11 -10
View File
@@ -11,7 +11,7 @@ jobs:
if: github.event.pull_request.merged == true && github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
@@ -22,16 +22,17 @@ jobs:
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
- name: Trigger pull request
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.CLOUD_DISPATCH }}
event-type: prowler-pull-request-merged
client-payload: '{
"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) }},
"PROWLER_PR_BODY": ${{ toJson(github.event.pull_request.body) }},
"PROWLER_PR_URL":${{ toJson(github.event.pull_request.html_url) }}
}'
client-payload: |
{
"PROWLER_COMMIT_SHA": "${{ github.event.pull_request.merge_commit_sha }}",
"PROWLER_COMMIT_SHORT_SHA": "${{ env.SHORT_SHA }}",
"PROWLER_PR_TITLE": ${{ toJson(github.event.pull_request.title) }},
"PROWLER_PR_LABELS": ${{ toJson(github.event.pull_request.labels.*.name) }},
"PROWLER_PR_BODY": ${{ toJson(github.event.pull_request.body) }},
"PROWLER_PR_URL": ${{ toJson(github.event.pull_request.html_url) }}
}
@@ -59,10 +59,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -108,13 +108,13 @@ jobs:
esac
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Public ECR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: public.ecr.aws
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
@@ -123,7 +123,7 @@ jobs:
AWS_REGION: ${{ env.AWS_REGION }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push container image (latest)
if: github.event_name == 'push'
@@ -157,6 +157,22 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
# - name: Push README to Docker Hub (toniblyx)
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# repository: ${{ env.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}
# readme-filepath: ./README.md
#
# - name: Push README to Docker Hub (prowlercloud)
# uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
# repository: ${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}
# readme-filepath: ./README.md
dispatch-action:
needs: container-build-push
runs-on: ubuntu-latest
+3 -2
View File
@@ -12,10 +12,9 @@ env:
jobs:
bump-version:
name: Bump Version
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get Prowler version
shell: bash
@@ -97,6 +96,7 @@ jobs:
commit-message: "chore(release): Bump version to v${{ env.BUMP_VERSION_TO }}"
branch: "version-bump-to-v${{ env.BUMP_VERSION_TO }}"
title: "chore(release): Bump version to v${{ env.BUMP_VERSION_TO }}"
labels: no-changelog
body: |
### Description
@@ -135,6 +135,7 @@ jobs:
commit-message: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}"
branch: "version-bump-to-v${{ env.PATCH_VERSION_TO }}"
title: "chore(release): Bump version to v${{ env.PATCH_VERSION_TO }}"
labels: no-changelog
body: |
### Description
+3 -3
View File
@@ -52,16 +52,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/sdk-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: "/language:${{matrix.language}}"
+56 -19
View File
@@ -21,11 +21,11 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: ./**
files_ignore: |
@@ -51,7 +51,7 @@ jobs:
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
@@ -102,20 +102,27 @@ jobs:
run: |
poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
- name: Dockerfile - Check if Dockerfile has changed
id: dockerfile-changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
Dockerfile
- name: Hadolint
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
if: steps.dockerfile-changed-files.outputs.any_changed == 'true'
run: |
/tmp/hadolint Dockerfile --ignore=DL3013
# Test AWS
- name: AWS - Check if any file has changed
id: aws-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/aws/**
./tests/providers/aws/**
.poetry.lock
./poetry.lock
- name: AWS - Test
if: steps.aws-changed-files.outputs.any_changed == 'true'
@@ -125,12 +132,12 @@ jobs:
# Test Azure
- name: Azure - Check if any file has changed
id: azure-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/azure/**
./tests/providers/azure/**
.poetry.lock
./poetry.lock
- name: Azure - Test
if: steps.azure-changed-files.outputs.any_changed == 'true'
@@ -140,12 +147,12 @@ jobs:
# Test GCP
- name: GCP - Check if any file has changed
id: gcp-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/gcp/**
./tests/providers/gcp/**
.poetry.lock
./poetry.lock
- name: GCP - Test
if: steps.gcp-changed-files.outputs.any_changed == 'true'
@@ -155,12 +162,12 @@ jobs:
# Test Kubernetes
- name: Kubernetes - Check if any file has changed
id: kubernetes-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/kubernetes/**
./tests/providers/kubernetes/**
.poetry.lock
./poetry.lock
- name: Kubernetes - Test
if: steps.kubernetes-changed-files.outputs.any_changed == 'true'
@@ -170,12 +177,12 @@ jobs:
# Test GitHub
- name: GitHub - Check if any file has changed
id: github-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/github/**
./tests/providers/github/**
.poetry.lock
./poetry.lock
- name: GitHub - Test
if: steps.github-changed-files.outputs.any_changed == 'true'
@@ -185,12 +192,12 @@ jobs:
# Test NHN
- name: NHN - Check if any file has changed
id: nhn-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/nhn/**
./tests/providers/nhn/**
.poetry.lock
./poetry.lock
- name: NHN - Test
if: steps.nhn-changed-files.outputs.any_changed == 'true'
@@ -200,18 +207,48 @@ jobs:
# Test M365
- name: M365 - Check if any file has changed
id: m365-changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/m365/**
./tests/providers/m365/**
.poetry.lock
./poetry.lock
- name: M365 - Test
if: steps.m365-changed-files.outputs.any_changed == 'true'
run: |
poetry run pytest -n auto --cov=./prowler/providers/m365 --cov-report=xml:m365_coverage.xml tests/providers/m365
# Test IaC
- name: IaC - Check if any file has changed
id: iac-changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/iac/**
./tests/providers/iac/**
./poetry.lock
- name: IaC - Test
if: steps.iac-changed-files.outputs.any_changed == 'true'
run: |
poetry run pytest -n auto --cov=./prowler/providers/iac --cov-report=xml:iac_coverage.xml tests/providers/iac
# Test MongoDB Atlas
- name: MongoDB Atlas - Check if any file has changed
id: mongodb-atlas-changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
with:
files: |
./prowler/providers/mongodbatlas/**
./tests/providers/mongodbatlas/**
.poetry.lock
- name: MongoDB Atlas - Test
if: steps.mongodb-atlas-changed-files.outputs.any_changed == 'true'
run: |
poetry run pytest -n auto --cov=./prowler/providers/mongodbatlas --cov-report=xml:mongodb_atlas_coverage.xml tests/providers/mongodbatlas
# Common Tests
- name: Lib - Test
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -226,7 +263,7 @@ jobs:
# Codecov
- name: Upload coverage reports to Codecov
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
+2 -2
View File
@@ -64,14 +64,14 @@ jobs:
;;
esac
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install dependencies
run: |
pipx install poetry==2.1.1
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.PYTHON_VERSION }}
# cache: ${{ env.CACHE }}
@@ -23,12 +23,12 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ env.GITHUB_BRANCH }}
- name: setup python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: 3.9 #install the python needed
@@ -38,7 +38,7 @@ jobs:
pip install boto3
- name: Configure AWS Credentials -- DEV
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
with:
aws-region: ${{ env.AWS_REGION_DEV }}
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
@@ -56,7 +56,7 @@ jobs:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
commit-message: "feat(regions_update): Update regions for AWS services"
branch: "aws-services-regions-updated-${{ github.sha }}"
labels: "status/waiting-for-revision, severity/low, provider/aws"
labels: "status/waiting-for-revision, severity/low, provider/aws, no-changelog"
title: "chore(regions_update): Changes in regions for AWS services"
body: |
### Description
@@ -30,6 +30,7 @@ env:
# Container Registries
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
NEXT_PUBLIC_API_BASE_URL: http://prowler-api:8080/api/v1
jobs:
repository-check:
@@ -61,7 +62,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set short git commit SHA
id: vars
@@ -70,13 +71,13 @@ jobs:
echo "SHORT_SHA=${shortSha}" >> $GITHUB_ENV
- name: Login to DockerHub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push container image (latest)
# Comment the following line for testing
@@ -86,6 +87,7 @@ jobs:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ env.SHORT_SHA }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
# Set push: false for testing
push: true
tags: |
@@ -101,6 +103,7 @@ jobs:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${{ env.RELEASE_TAG }}
NEXT_PUBLIC_API_BASE_URL=${{ env.NEXT_PUBLIC_API_BASE_URL }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
@@ -110,7 +113,7 @@ jobs:
- name: Trigger deployment
if: github.event_name == 'push'
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.CLOUD_DISPATCH }}
+3 -3
View File
@@ -44,16 +44,16 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/ui-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
with:
category: "/language:${{matrix.language}}"
+105
View File
@@ -0,0 +1,105 @@
name: UI - E2E Tests
on:
pull_request:
branches:
- master
- "v5.*"
paths:
- '.github/workflows/ui-e2e-tests.yml'
- 'ui/**'
jobs:
e2e-tests:
if: github.repository == 'prowler-cloud/prowler'
runs-on: ubuntu-latest
env:
AUTH_SECRET: 'fallback-ci-secret-for-testing'
AUTH_TRUST_HOST: true
NEXTAUTH_URL: 'http://localhost:3000'
NEXT_PUBLIC_API_BASE_URL: 'http://localhost:8080/api/v1'
E2E_GITHUB_USER: ${{ secrets.E2E_GITHUB_USER }}
E2E_GITHUB_PASSWORD: ${{ secrets.E2E_GITHUB_PASSWORD }}
SOCIAL_GITHUB_OAUTH_CLIENT_ID: ${{ secrets.E2E_SOCIAL_GITHUB_OAUTH_CLIENT_ID }}
SOCIAL_GITHUB_OAUTH_CLIENT_SECRET: ${{ secrets.E2E_SOCIAL_GITHUB_OAUTH_CLIENT_SECRET }}
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Fix API data directory permissions
run: docker run --rm -v $(pwd)/_data/api:/data alpine chown -R 1000:1000 /data
- name: Start API services
run: |
# Override docker-compose image tag to use latest instead of stable
# This overrides any PROWLER_API_VERSION set in .env file
export PROWLER_API_VERSION=latest
echo "Using PROWLER_API_VERSION=${PROWLER_API_VERSION}"
docker compose up -d api worker worker-beat
- name: Wait for API to be ready
run: |
echo "Waiting for prowler-api..."
timeout=150 # 5 minutes max
elapsed=0
while [ $elapsed -lt $timeout ]; do
if curl -s ${NEXT_PUBLIC_API_BASE_URL}/docs >/dev/null 2>&1; then
echo "Prowler API is ready!"
exit 0
fi
echo "Waiting for prowler-api... (${elapsed}s elapsed)"
sleep 5
elapsed=$((elapsed + 5))
done
echo "Timeout waiting for prowler-api to start"
exit 1
- name: Load database fixtures for E2E tests
run: |
docker compose exec -T api sh -c '
echo "Loading all fixtures from api/fixtures/dev/..."
for fixture in api/fixtures/dev/*.json; do
if [ -f "$fixture" ]; then
echo "Loading $fixture"
poetry run python manage.py loaddata "$fixture" --database admin
fi
done
echo "All database fixtures loaded successfully!"
'
- name: Setup Node.js environment
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: '20.x'
cache: 'npm'
cache-dependency-path: './ui/package-lock.json'
- name: Install UI dependencies
working-directory: ./ui
run: npm ci
- name: Build UI application
working-directory: ./ui
run: npm run build
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('ui/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browsers
working-directory: ./ui
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npm run test:e2e:install
- name: Run E2E tests
working-directory: ./ui
run: npm run test:e2e
- name: Upload test reports
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: failure()
with:
name: playwright-report
path: ui/playwright-report/
retention-days: 30
- name: Cleanup services
if: always()
run: |
echo "Shutting down services..."
docker compose down -v || true
echo "Cleanup completed"
+8 -5
View File
@@ -27,28 +27,31 @@ jobs:
node-version: [20.x]
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: './ui/package-lock.json'
- name: Install dependencies
working-directory: ./ui
run: npm install
run: npm ci
- name: Run Healthcheck
working-directory: ./ui
run: npm run healthcheck
- name: Build the application
working-directory: ./ui
run: npm run build
test-container-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build Container
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
+18
View File
@@ -44,6 +44,16 @@ junit-reports/
# Cursor files
.cursorignore
.cursor/
# RooCode files
.roo/
.rooignore
.roomodes
# Cline files
.cline/
.clineignore
# Terraform
.terraform*
@@ -53,6 +63,7 @@ junit-reports/
# .env
ui/.env*
api/.env*
mcp_server/.env*
.env.local
# Coverage
@@ -65,3 +76,10 @@ node_modules
# Persistent data
_data/
# Claude
CLAUDE.md
# MCP Server
mcp_server/prowler_mcp_server/prowler_app/server.py
mcp_server/prowler_mcp_server/prowler_app/utils/schema.yaml
+3 -1
View File
@@ -6,6 +6,7 @@ repos:
- id: check-merge-conflict
- id: check-yaml
args: ["--unsafe"]
exclude: prowler/config/llm_config.yaml
- id: check-json
- id: end-of-file-fixer
- id: trailing-whitespace
@@ -115,7 +116,8 @@ repos:
- id: safety
name: safety
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
entry: bash -c 'safety check --ignore 70612,66963,74429'
# TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745'
language: system
- id: vulture
+110
View File
@@ -0,0 +1,110 @@
# Repository Guidelines
## How to Use This Guide
- Start here for cross-project norms, Prowler is a monorepo with several components. Every component should have an `AGENTS.md` file that contains the guidelines for the agents in that component. The file is located beside the code you are touching (e.g. `api/AGENTS.md`, `ui/AGENTS.md`, `prowler/AGENTS.md`).
- Follow the stricter rule when guidance conflicts; component docs override this file for their scope.
- Keep instructions synchronized. When you add new workflows or scripts, update both, the relevant component `AGENTS.md` and this file if they apply broadly.
## Project Overview
Prowler is an open-source cloud security assessment tool that supports multiple cloud providers (AWS, Azure, GCP, Kubernetes, GitHub, M365, etc.). The project consists in a monorepo with the following main components:
- **Prowler SDK**: Python SDK, includes the Prowler CLI, providers, services, checks, compliances, config, etc. (`prowler/`)
- **Prowler API**: Django-based REST API backend (`api/`)
- **Prowler UI**: Next.js frontend application (`ui/`)
- **Prowler MCP Server**: Model Context Protocol server that gives access to the entire Prowler ecosystem for LLMs (`mcp_server/`)
- **Prowler Dashboard**: Prowler CLI feature that allows to visualize the results of the scans in a simple dashboard (`dashboard/`)
### Project Structure (Key Folders & Files)
- `prowler/`: Main source code for Prowler SDK (CLI, providers, services, checks, compliances, config, etc.)
- `api/`: Django-based REST API backend components
- `ui/`: Next.js frontend application
- `mcp_server/`: Model Context Protocol server that gives access to the entire Prowler ecosystem for LLMs
- `dashboard/`: Prowler CLI feature that allows to visualize the results of the scans in a simple dashboard
- `docs/`: Documentation
- `examples/`: Example output formats for providers and scripts
- `permissions/`: Permission-related files and policies
- `contrib/`: Community-contributed scripts or modules
- `tests/`: Prowler SDK test suite
- `docker-compose.yml`: Docker compose file to run the Prowler App (API + UI) production environment
- `docker-compose-dev.yml`: Docker compose file to run the Prowler App (API + UI) development environment
- `pyproject.toml`: Poetry Prowler SDK project file
- `.pre-commit-config.yaml`: Pre-commit hooks configuration
- `Makefile`: Makefile to run the project
- `LICENSE`: License file
- `README.md`: README file
- `CONTRIBUTING.md`: Contributing guide
## Python Development
Most of the code is written in Python, so the main files in the root are focused on Python code.
### Poetry Dev Environment
For developing in Python we recommend using `poetry` to manage the dependencies. The minimal version is `2.1.1`. So it is recommended to run all commands using `poetry run ...`.
To install the core dependencies to develop it is needed to run `poetry install --with dev`.
### Pre-commit hooks
The project has pre-commit hooks to lint and format the code. They are installed by running `poetry run pre-commit install`.
When commiting a change, the hooks will be run automatically. Some of them are:
- Code formatting (black, isort)
- Linting (flake8, pylint)
- Security checks (bandit, safety, trufflehog)
- YAML/JSON validation
- Poetry lock file validation
### Linting and Formatting
We use the following tools to lint and format the code:
- `flake8`: for linting the code
- `black`: for formatting the code
- `pylint`: for linting the code
You can run all using the `make` command:
```bash
poetry run make lint
poetry run make format
```
Or they will be run automatically when you commit your changes using pre-commit hooks.
## Commit & Pull Request Guidelines
For the commit messages and pull requests name follow the conventional-commit style.
Befire creating a pull request, complete the checklist in `.github/pull_request_template.md`. Summaries should explain deployment impact, highlight review steps, and note changelog or permission updates. Run all relevant tests and linters before requesting review and link screenshots for UI or dashboard changes.
### Conventional Commit Style
The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of.
The commit message should be structured as follows:
```
<type>[optional scope]: <description>
<BLANK LINE>
[optional body]
<BLANK LINE>
[optional footer(s)]
```
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools
#### Commit Types
- **feat**: code change introuce new functionality to the application
- **fix**: code change that solve a bug in the codebase
- **docs**: documentation only changes
- **chore**: changes related to the build process or auxiliary tools and libraries, that do not affect the application's functionality
- **perf**: code change that improves performance
- **refactor**: code change that neither fixes a bug nor adds a feature
- **style**: changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- **test**: adding missing tests or correcting existing tests
+3 -6
View File
@@ -1,4 +1,4 @@
FROM python:3.12.10-slim-bookworm AS build
FROM python:3.12.11-slim-bookworm AS build
LABEL maintainer="https://github.com/prowler-cloud/prowler"
LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
@@ -6,7 +6,8 @@ LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler"
ARG POWERSHELL_VERSION=7.5.0
# hadolint ignore=DL3008
RUN apt-get update && apt-get install -y --no-install-recommends wget libicu72 \
RUN apt-get update && apt-get install -y --no-install-recommends \
wget libicu72 libunwind8 libssl3 libcurl4 ca-certificates apt-transport-https gnupg \
&& rm -rf /var/lib/apt/lists/*
# Install PowerShell
@@ -46,10 +47,6 @@ ENV PATH="${HOME}/.local/bin:${PATH}"
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir poetry
# By default poetry does not compile Python source files to bytecode during installation.
# This speeds up the installation process, but the first execution may take a little more
# time because Python then compiles source files to bytecode automatically. If you want to
# compile source files to bytecode during installation, you can use the --compile option
RUN poetry install --compile && \
rm -rf ~/.cache/pip
+4
View File
@@ -45,3 +45,7 @@ pypi-upload: ## Upload package
help: ## Show this help.
@echo "Prowler Makefile"
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Development Environment
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat --build
+37 -55
View File
@@ -19,19 +19,16 @@
<a href="https://goto.prowler.com/slack"><img alt="Slack Shield" src="https://img.shields.io/badge/slack-prowler-brightgreen.svg?logo=slack"></a>
<a href="https://pypi.org/project/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/v/prowler.svg"></a>
<a href="https://pypi.python.org/pypi/prowler/"><img alt="Python Version" src="https://img.shields.io/pypi/pyversions/prowler.svg"></a>
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Prowler Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=prowler%20downloads"></a>
<a href="https://pypistats.org/packages/prowler"><img alt="PyPI Downloads" src="https://img.shields.io/pypi/dw/prowler.svg?label=downloads"></a>
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/toniblyx/prowler"></a>
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/cloud/build/toniblyx/prowler"></a>
<a href="https://hub.docker.com/r/toniblyx/prowler"><img alt="Docker" src="https://img.shields.io/docker/image-size/toniblyx/prowler"></a>
<a href="https://gallery.ecr.aws/prowler-cloud/prowler"><img width="120" height=19" alt="AWS ECR Gallery" src="https://user-images.githubusercontent.com/3985464/151531396-b6535a68-c907-44eb-95a1-a09508178616.png"></a>
<a href="https://codecov.io/gh/prowler-cloud/prowler"><img src="https://codecov.io/gh/prowler-cloud/prowler/graph/badge.svg?token=OflBGsdpDl"/></a>
</p>
<p align="center">
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/release-date/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler"><img alt="Contributors" src="https://img.shields.io/github/contributors-anon/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler"><img alt="License" src="https://img.shields.io/github/license/prowler-cloud/prowler"></a>
<a href="https://twitter.com/ToniBlyx"><img alt="Twitter" src="https://img.shields.io/twitter/follow/toniblyx?style=social"></a>
<a href="https://twitter.com/prowlercloud"><img alt="Twitter" src="https://img.shields.io/twitter/follow/prowlercloud?style=social"></a>
@@ -55,15 +52,11 @@ Prowler includes hundreds of built-in controls to ensure compliance with standar
- **National Security Standards:** ENS (Spanish National Security Scheme)
- **Custom Security Frameworks:** Tailored to your needs
## Prowler CLI and Prowler Cloud
Prowler offers a Command Line Interface (CLI), known as Prowler Open Source, and an additional service built on top of it, called <a href="https://prowler.com">Prowler Cloud</a>.
## Prowler App
Prowler App is a web-based application that simplifies running Prowler across your cloud provider accounts. It provides a user-friendly interface to visualize the results and streamline your security assessments.
![Prowler App](docs/img/overview.png)
![Prowler App](docs/products/img/overview.png)
>For more details, refer to the [Prowler App Documentation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#prowler-app-installation)
@@ -80,28 +73,37 @@ prowler <provider>
```console
prowler dashboard
```
![Prowler Dashboard](docs/img/dashboard.png)
![Prowler Dashboard](docs/products/img/dashboard.png)
# Prowler at a Glance
> [!Tip]
> For the most accurate and up-to-date information about checks, services, frameworks, and categories, visit [**Prowler Hub**](https://hub.prowler.com).
| 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 | 567 | 82 | 36 | 10 |
| GCP | 79 | 13 | 10 | 3 |
| Azure | 142 | 18 | 10 | 3 |
| Kubernetes | 83 | 7 | 5 | 7 |
| GitHub | 16 | 2 | 1 | 0 |
| M365 | 69 | 7 | 2 | 2 |
| NHN (Unofficial) | 6 | 2 | 1 | 0 |
| 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) | Support | Stage | Interface |
|---|---|---|---|---|---|---|---|
| AWS | 576 | 82 | 36 | 10 | Official | Stable | UI, API, CLI |
| GCP | 79 | 13 | 10 | 3 | Official | Stable | UI, API, CLI |
| Azure | 162 | 19 | 11 | 4 | Official | Stable | UI, API, CLI |
| Kubernetes | 83 | 7 | 5 | 7 | Official | Stable | UI, API, CLI |
| GitHub | 17 | 2 | 1 | 0 | Official | Stable | UI, API, CLI |
| M365 | 70 | 7 | 3 | 2 | Official | Stable | UI, API, CLI |
| IaC | [See `trivy` docs.](https://trivy.dev/latest/docs/coverage/iac/) | N/A | N/A | N/A | Official | Beta | CLI |
| MongoDB Atlas | 10 | 3 | 0 | 0 | Official | Beta | CLI |
| LLM | [See `promptfoo` docs.](https://www.promptfoo.dev/docs/red-team/plugins/) | N/A | N/A | N/A | Official | Beta | CLI |
| NHN | 6 | 2 | 1 | 0 | Unofficial | Beta | CLI |
> [!Note]
> The numbers in the table are updated periodically.
> [!Tip]
> For the most accurate and up-to-date information about checks, services, frameworks, and categories, visit [**Prowler Hub**](https://hub.prowler.com).
> [!Note]
> Use the following commands to list Prowler's available checks, services, compliance frameworks, and categories: `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
> Use the following commands to list Prowler's available checks, services, compliance frameworks, and categories:
> - `prowler <provider> --list-checks`
> - `prowler <provider> --list-services`
> - `prowler <provider> --list-compliance`
> - `prowler <provider> --list-categories`
# 💻 Installation
@@ -136,6 +138,14 @@ If your workstation's architecture is incompatible, you can resolve this by:
> Once configured, access the Prowler App at http://localhost:3000. Sign up using your email and password to get started.
### Common Issues with Docker Pull Installation
> [!Note]
If you want to use AWS role assumption (e.g., with the "Connect assuming IAM Role" option), you may need to mount your local `.aws` directory into the container as a volume (e.g., `- "${HOME}/.aws:/home/prowler/.aws:ro"`). There are several ways to configure credentials for Docker containers. See the [Troubleshooting](./docs/troubleshooting.md) section for more details and examples.
You can find more information in the [Troubleshooting](./docs/troubleshooting.md) section.
### From GitHub
**Requirements**
@@ -231,7 +241,7 @@ The following versions of Prowler CLI are available, depending on your requireme
The container images are available here:
- Prowler CLI:
- [DockerHub](https://hub.docker.com/r/toniblyx/prowler/tags)
- [DockerHub](https://hub.docker.com/r/prowlercloud/prowler/tags)
- [AWS Public ECR](https://gallery.ecr.aws/prowler-cloud/prowler)
- Prowler App:
- [DockerHub - Prowler UI](https://hub.docker.com/r/prowlercloud/prowler-ui/tags)
@@ -266,7 +276,7 @@ python prowler-cli.py -v
- **Prowler API**: A backend service, developed with Django REST Framework, responsible for running Prowler scans and storing the generated results.
- **Prowler SDK**: A Python SDK designed to extend the functionality of the Prowler CLI for advanced capabilities.
![Prowler App Architecture](docs/img/prowler-app-architecture.png)
![Prowler App Architecture](docs/products/img/prowler-app-architecture.png)
## Prowler CLI
@@ -292,40 +302,12 @@ And many more environments.
![Architecture](docs/img/architecture.png)
# Deprecations from v3
## General
- `Allowlist` now is called `Mutelist`.
- The `--quiet` option has been deprecated. Use the `--status` flag to filter findings based on their status: PASS, FAIL, or MANUAL.
- All findings with an `INFO` status have been reclassified as `MANUAL`.
- The CSV output format is standardized across all providers.
**Deprecated Output Formats**
The following formats are now deprecated:
- Native JSON has been replaced with JSON in [OCSF] v1.1.0 format, which is standardized across all providers (https://schema.ocsf.io/).
## AWS
**AWS Flag Deprecation**
The flag --sts-endpoint-region has been deprecated due to the adoption of AWS STS regional tokens.
**Sending FAIL Results to AWS Security Hub**
- To send only FAILS to AWS Security Hub, use one of the following options: `--send-sh-only-fails` or `--security-hub --status FAIL`.
# 📖 Documentation
**Documentation Resources**
For installation instructions, usage details, tutorials, and the Developer Guide, visit https://docs.prowler.com/
# 📃 License
**Prowler License Information**
Prowler is licensed under the Apache License 2.0, as indicated in each file within the repository. Obtaining a Copy of the License
Prowler is licensed under the Apache License 2.0.
A copy of the License is available at <http://www.apache.org/licenses/LICENSE-2.0>
+57 -15
View File
@@ -1,23 +1,65 @@
# Security Policy
# Security
## Software Security
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
## Reporting Vulnerabilities
- `bandit` for code security review.
- `safety` and `dependabot` for dependencies.
- `hadolint` and `dockle` for our containers security.
- `snyk` in Docker Hub.
- `clair` in Amazon ECR.
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
At Prowler, we consider the security of our open source software and systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
## Reporting a Vulnerability
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our users, our clients and our systems.
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to https://support.prowler.com.
When reporting vulnerabilities, please consider (1) attack scenario / exploitability, and (2) the security impact of the bug. The following issues are considered out of scope:
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
- Social engineering support or attacks requiring social engineering.
- Clickjacking on pages with no sensitive actions.
- Cross-Site Request Forgery (CSRF) on unauthenticated forms or forms with no sensitive actions.
- Attacks requiring Man-In-The-Middle (MITM) or physical access to a user's device.
- Previously known vulnerable libraries without a working Proof of Concept (PoC).
- Comma Separated Values (CSV) injection without demonstrating a vulnerability.
- Missing best practices in SSL/TLS configuration.
- Any activity that could lead to the disruption of service (DoS).
- Rate limiting or brute force issues on non-authentication endpoints.
- Missing best practices in Content Security Policy (CSP).
- Missing HttpOnly or Secure flags on cookies.
- Configuration of or missing security headers.
- Missing email best practices, such as invalid, incomplete, or missing SPF/DKIM/DMARC records.
- Vulnerabilities only affecting users of outdated or unpatched browsers (less than two stable versions behind).
- Software version disclosure, banner identification issues, or descriptive error messages.
- Tabnabbing.
- Issues that require unlikely user interaction.
- Improper logout functionality and improper session timeout.
- CORS misconfiguration without an exploitation scenario.
- Broken link hijacking.
- Automated scanning results (e.g., sqlmap, Burp active scanner) that have not been manually verified.
- Content spoofing and text injection issues without a clear attack vector.
- Email spoofing without exploiting security flaws.
- Dead links or broken links.
- User enumeration.
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
Testing guidelines:
- Do not run automated scanners on other customer projects. Running automated scanners can run up costs for our users. Aggressively configured scanners might inadvertently disrupt services, exploit vulnerabilities, lead to system instability or breaches and violate Terms of Service from our upstream providers. Our own security systems won't be able to distinguish hostile reconnaissance from whitehat research. If you wish to run an automated scanner, notify us at support@prowler.com and only run it on your own Prowler app project. Do NOT attack Prowler in usage of other customers.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data.
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
Reporting guidelines:
- File a report through our Support Desk at https://support.prowler.com
- If it is about a lack of a security functionality, please file a feature request instead at https://github.com/prowler-cloud/prowler/issues
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible.
- If you have further questions and want direct interaction with the Prowler team, please contact us at via our Community Slack at goto.prowler.com/slack.
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
Disclosure guidelines:
- In order to protect our users and customers, do not reveal the problem to others until we have researched, addressed and informed our affected customers.
- If you want to publicly share your research about Prowler at a conference, in a blog or any other public forum, you should share a draft with us for review and approval at least 30 days prior to the publication date. Please note that the following should not be included:
- Data regarding any Prowler user or customer projects.
- Prowler customers' data.
- Information about Prowler employees, contractors or partners.
What we promise:
- We will respond to your report within 5 business days with our evaluation of the report and an expected resolution date.
- If you have followed the instructions above, we will not take any legal action against you in regard to the report.
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission.
- We will keep you informed of the progress towards resolving the problem.
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise).
We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
---
For more information about our security policies, please refer to our [Security](https://docs.prowler.com/projects/prowler-open-source/en/latest/security/) section in our documentation.
+2
View File
@@ -19,6 +19,8 @@ DJANGO_REFRESH_TOKEN_LIFETIME=1440
DJANGO_CACHE_MAX_AGE=3600
DJANGO_STALE_WHILE_REVALIDATE=60
DJANGO_SECRETS_ENCRYPTION_KEY=""
# Throttle, two options: Empty means no throttle; or if desired use one in DRF format: https://www.django-rest-framework.org/api-guide/throttling/#setting-the-throttling-policy
DJANGO_THROTTLE_TOKEN_OBTAIN=50/minute
# Decide whether to allow Django manage database table partitions
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
-168
View File
@@ -1,168 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/_data/
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
*.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# VSCode
.vscode/
-91
View File
@@ -1,91 +0,0 @@
repos:
## GENERAL
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-merge-conflict
- id: check-yaml
args: ["--unsafe"]
- id: check-json
- id: end-of-file-fixer
- id: trailing-whitespace
- id: no-commit-to-branch
- id: pretty-format-json
args: ["--autofix", "--no-sort-keys", "--no-ensure-ascii"]
exclude: 'src/backend/api/fixtures/dev/.*\.json$'
## TOML
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.13.0
hooks:
- id: pretty-format-toml
args: [--autofix]
files: pyproject.toml
## BASH
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck
exclude: contrib
## PYTHON
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.0
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: https://github.com/python-poetry/poetry
rev: 1.8.0
hooks:
- id: poetry-check
args: ["--directory=src"]
- id: poetry-lock
args: ["--no-update", "--directory=src"]
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
hooks:
- id: hadolint
args: ["--ignore=DL3013", "Dockerfile"]
- repo: local
hooks:
- id: pylint
name: pylint
entry: bash -c 'poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/'
language: system
files: '.*\.py'
- id: trufflehog
name: TruffleHog
description: Detect secrets in your data.
entry: bash -c 'trufflehog --no-update git file://. --only-verified --fail'
# For running trufflehog in docker, use the following entry instead:
# entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
language: system
stages: ["commit", "push"]
- id: bandit
name: bandit
description: "Bandit is a tool for finding common security issues in Python code"
entry: bash -c 'poetry run bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
language: system
files: '.*\.py'
- id: safety
name: safety
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
entry: bash -c 'poetry run safety check --ignore 70612,66963,74429'
language: system
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
entry: bash -c 'poetry run vulture --exclude "contrib,.venv,tests,conftest.py" --min-confidence 100 .'
language: system
files: '.*\.py'
+129 -2
View File
@@ -2,16 +2,143 @@
All notable changes to the **Prowler API** are documented in this file.
## [v1.9.0] (Prowler UNRELEASED)
## [1.14.0] (Prowler UNRELEASED)
### Added
- Default JWT keys are generated and stored if they are missing from configuration [(#8655)](https://github.com/prowler-cloud/prowler/pull/8655)
- `compliance_name` for each compliance [(#7920)](https://github.com/prowler-cloud/prowler/pull/7920)
- Support for M365 Certificate authentication [(#8538)](https://github.com/prowler-cloud/prowler/pull/8538)
- API Key support [(#8805)](https://github.com/prowler-cloud/prowler/pull/8805)
- SAML role mapping protection for single-admin tenants to prevent accidental lockout [(#8882)](https://github.com/prowler-cloud/prowler/pull/8882)
- Support for `passed_findings` and `total_findings` fields in compliance requirement overview for accurate Prowler ThreatScore calculation [(#8582)](https://github.com/prowler-cloud/prowler/pull/8582)
- Database read replica support [(#8869)](https://github.com/prowler-cloud/prowler/pull/8869)
### Changed
- Now the MANAGE_ACCOUNT permission is required to modify or read user permissions instead of MANAGE_USERS [(#8281)](https://github.com/prowler-cloud/prowler/pull/8281)
- Now at least one user with MANAGE_ACCOUNT permission is required in the tenant [(#8729)](https://github.com/prowler-cloud/prowler/pull/8729)
### Security
- Django updated to the latest 5.1 security release, 5.1.13, due to problems with potential [SQL injection](https://github.com/prowler-cloud/prowler/security/dependabot/104) and [directory traversals](https://github.com/prowler-cloud/prowler/security/dependabot/103) [(#8842)](https://github.com/prowler-cloud/prowler/pull/8842)
---
## [1.13.2] (Prowler 5.12.3)
### Fixed
- 500 error when deleting user [(#8731)](https://github.com/prowler-cloud/prowler/pull/8731)
---
## [1.13.1] (Prowler 5.12.2)
### Changed
- Renamed compliance overview task queue to `compliance` [(#8755)](https://github.com/prowler-cloud/prowler/pull/8755)
### Security
- Django updated to the latest 5.1 security release, 5.1.12, due to [problems](https://www.djangoproject.com/weblog/2025/sep/03/security-releases/) with potential SQL injection in FilteredRelation column aliases [(#8693)](https://github.com/prowler-cloud/prowler/pull/8693)
---
## [1.13.0] (Prowler 5.12.0)
### Added
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
- `GET /overviews/findings_severity` now supports `filter[status]` and `filter[status__in]` to aggregate by specific statuses (`FAIL`, `PASS`)[(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
- Throttling options for `/api/v1/tokens` using the `DJANGO_THROTTLE_TOKEN_OBTAIN` environment variable [(#8647)](https://github.com/prowler-cloud/prowler/pull/8647)
---
## [1.12.0] (Prowler 5.11.0)
### Added
- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527)
- Integration with Amazon Security Hub, enabling sending findings to Security Hub [(#8365)](https://github.com/prowler-cloud/prowler/pull/8365)
- Generate ASFF output for AWS providers with SecurityHub integration enabled [(#8569)](https://github.com/prowler-cloud/prowler/pull/8569)
### Fixed
- GitHub provider always scans user instead of organization when using provider UID [(#8587)](https://github.com/prowler-cloud/prowler/pull/8587)
---
## [1.11.0] (Prowler 5.10.0)
### Added
- Github provider support [(#8271)](https://github.com/prowler-cloud/prowler/pull/8271)
- Integration with Amazon S3, enabling storage and retrieval of scan data via S3 buckets [(#8056)](https://github.com/prowler-cloud/prowler/pull/8056)
### Fixed
- Avoid sending errors to Sentry in M365 provider when user authentication fails [(#8420)](https://github.com/prowler-cloud/prowler/pull/8420)
---
## [1.10.2] (Prowler v5.9.2)
### Changed
- Optimized queries for resources views [(#8336)](https://github.com/prowler-cloud/prowler/pull/8336)
---
## [v1.10.1] (Prowler v5.9.1)
### Fixed
- Calculate failed findings during scans to prevent heavy database queries [(#8322)](https://github.com/prowler-cloud/prowler/pull/8322)
---
## [v1.10.0] (Prowler v5.9.0)
### Added
- SSO with SAML support [(#8175)](https://github.com/prowler-cloud/prowler/pull/8175)
- `GET /resources/metadata`, `GET /resources/metadata/latest` and `GET /resources/latest` to expose resource metadata and latest scan results [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
### Changed
- `/processors` endpoints to post-process findings. Currently, only the Mutelist processor is supported to allow to mute findings.
- Optimized the underlying queries for resources endpoints [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- Optimized include parameters for resources view [(#8229)](https://github.com/prowler-cloud/prowler/pull/8229)
- Optimized overview background tasks [(#8300)](https://github.com/prowler-cloud/prowler/pull/8300)
### Fixed
- Search filter for findings and resources [(#8112)](https://github.com/prowler-cloud/prowler/pull/8112)
- RBAC is now applied to `GET /overviews/providers` [(#8277)](https://github.com/prowler-cloud/prowler/pull/8277)
### Changed
- `POST /schedules/daily` returns a `409 CONFLICT` if already created [(#8258)](https://github.com/prowler-cloud/prowler/pull/8258)
### Security
- Enhanced password validation to enforce 12+ character passwords with special characters, uppercase, lowercase, and numbers [(#8225)](https://github.com/prowler-cloud/prowler/pull/8225)
---
## [v1.9.1] (Prowler v5.8.1)
### Added
- Custom exception for provider connection errors during scans [(#8234)](https://github.com/prowler-cloud/prowler/pull/8234)
### Changed
- Summary and overview tasks now use a dedicated queue and no longer propagate errors to compliance tasks [(#8214)](https://github.com/prowler-cloud/prowler/pull/8214)
### Fixed
- Scan with no resources will not trigger legacy code for findings metadata [(#8183)](https://github.com/prowler-cloud/prowler/pull/8183)
- Invitation email comparison case-insensitive [(#8206)](https://github.com/prowler-cloud/prowler/pull/8206)
### Removed
- Validation of the provider's secret type during updates [(#8197)](https://github.com/prowler-cloud/prowler/pull/8197)
---
## [v1.9.0] (Prowler v5.8.0)
### Added
- SSO with SAML support [(#7822)](https://github.com/prowler-cloud/prowler/pull/7822)
- Support GCP Service Account key [(#7824)](https://github.com/prowler-cloud/prowler/pull/7824)
- `GET /compliance-overviews` endpoints to retrieve compliance metadata and specific requirements statuses [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
- Lighthouse configuration support [(#7848)](https://github.com/prowler-cloud/prowler/pull/7848)
### Changed
- Reworked `GET /compliance-overviews` to return proper requirement metrics [(#7877)](https://github.com/prowler-cloud/prowler/pull/7877)
- Optional `user` and `password` for M365 provider [(#7992)](https://github.com/prowler-cloud/prowler/pull/7992)
### Fixed
- Scheduled scans are no longer deleted when their daily schedule run is disabled [(#8082)](https://github.com/prowler-cloud/prowler/pull/8082)
---
+3 -4
View File
@@ -44,6 +44,9 @@ USER prowler
WORKDIR /home/prowler
# Ensure output directory exists
RUN mkdir -p /tmp/prowler_api_output
COPY pyproject.toml ./
RUN pip install --no-cache-dir --upgrade pip && \
@@ -57,10 +60,6 @@ RUN poetry install --no-root && \
RUN poetry run python "$(poetry env info --path)/src/prowler/prowler/providers/m365/lib/powershell/m365_powershell.py"
# Prevents known compatibility error between lxml and libxml2/libxmlsec versions.
# See: https://github.com/xmlsec/python-xmlsec/issues/320
RUN poetry run pip install --force-reinstall --no-binary lxml lxml
COPY src/backend/ ./backend/
COPY docker-entrypoint.sh ./docker-entrypoint.sh
+6 -2
View File
@@ -18,7 +18,11 @@ Valkey exposes a Redis 7.2 compliant API. Any service that exposes the Redis API
# Modify environment variables
Under the root path of the project, you can find a file called `.env.example`. This file shows all the environment variables that the project uses. You *must* create a new file called `.env` and set the values for the variables.
Under the root path of the project, you can find a file called `.env`. This file shows all the environment variables that the project uses. You should review it and set the values for the variables you want to change.
If you dont set `DJANGO_TOKEN_SIGNING_KEY` or `DJANGO_TOKEN_VERIFYING_KEY`, the API will generate them at `~/.config/prowler-api/` with `0600` and `0644` permissions; back up these files to persist identity across redeploys.
**Important note**: Every Prowler version (or repository branches and tags) could have different variables set in its `.env` file. Please use the `.env` file that corresponds with each version.
## Local deployment
Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the Poetry interpreter, not before. Otherwise, variables will not be loaded properly.
@@ -257,7 +261,7 @@ cd src/backend
python manage.py loaddata api/fixtures/0_dev_users.json --database admin
```
> The default credentials are `dev@prowler.com:thisisapassword123` or `dev2@prowler.com:thisisapassword123`
> The default credentials are `dev@prowler.com:Thisisapassword123@` or `dev2@prowler.com:Thisisapassword123@`
## Run tests
+1 -1
View File
@@ -32,7 +32,7 @@ start_prod_server() {
start_worker() {
echo "Starting the worker..."
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill -E --max-tasks-per-child 1
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion,backfill,overview,integrations,compliance -E --max-tasks-per-child 1
}
start_worker_beat() {
+1837 -1399
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -7,7 +7,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}]
dependencies = [
"celery[pytest] (>=5.4.0,<6.0.0)",
"dj-rest-auth[with_social,jwt] (==7.0.1)",
"django==5.1.10",
"django (==5.1.13)",
"django-allauth[saml] (>=65.8.0,<66.0.0)",
"django-celery-beat (>=2.7.0,<3.0.0)",
"django-celery-results (>=2.5.1,<3.0.0)",
@@ -23,12 +23,17 @@ dependencies = [
"drf-spectacular==0.27.2",
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"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)"
"openai (>=1.82.0,<2.0.0)",
"xmlsec==1.3.14",
"h2 (==4.3.0)",
"markdown (>=3.9,<4.0)",
"drf-simple-apikey (==2.2.1)"
]
description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
@@ -36,7 +41,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.9.0"
version = "1.14.0"
[project.scripts]
celery = "src.backend.config.settings.celery"
+6 -58
View File
@@ -3,14 +3,7 @@ from django.db import transaction
from api.db_router import MainRouter
from api.db_utils import rls_transaction
from api.models import (
Membership,
Role,
SAMLConfiguration,
Tenant,
User,
UserRoleRelationship,
)
from api.models import Membership, Role, Tenant, User, UserRoleRelationship
class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
@@ -24,7 +17,7 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
# Link existing accounts with the same email address
email = sociallogin.account.extra_data.get("email")
if sociallogin.account.provider == "saml":
if sociallogin.provider.id == "saml":
email = sociallogin.user.email
if email:
existing_user = self.get_user_by_email(email)
@@ -38,57 +31,10 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
"""
with transaction.atomic(using=MainRouter.admin_db):
user = super().save_user(request, sociallogin, form)
provider = sociallogin.account.provider
provider = sociallogin.provider.id
extra = sociallogin.account.extra_data
if provider == "saml":
# Handle SAML-specific logic
user.first_name = extra.get("firstName", [""])[0]
user.last_name = extra.get("lastName", [""])[0]
user.company_name = extra.get("organization", [""])[0]
user.name = f"{user.first_name} {user.last_name}".strip()
user.save(using=MainRouter.admin_db)
email_domain = user.email.split("@")[-1]
tenant = (
SAMLConfiguration.objects.using(MainRouter.admin_db)
.get(email_domain=email_domain)
.tenant
)
with rls_transaction(str(tenant.id)):
role_name = extra.get("userType", ["saml_default_role"])[0].strip()
try:
role = Role.objects.using(MainRouter.admin_db).get(
name=role_name, tenant_id=tenant.id
)
except Role.DoesNotExist:
role = Role.objects.using(MainRouter.admin_db).create(
name=role_name,
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
Membership.objects.using(MainRouter.admin_db).create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.MEMBER,
)
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
user=user,
role=role,
tenant_id=tenant.id,
)
else:
if provider != "saml":
# Handle other providers (e.g., GitHub, Google)
user.save(using=MainRouter.admin_db)
social_account_name = extra.get("name")
@@ -119,5 +65,7 @@ class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
role=role,
tenant_id=tenant.id,
)
else:
request.session["saml_user_created"] = str(user.id)
return user
+157
View File
@@ -1,4 +1,26 @@
import logging
import os
import sys
from pathlib import Path
from config.custom_logging import BackendLogger
from config.env import env
from django.apps import AppConfig
from django.conf import settings
logger = logging.getLogger(BackendLogger.API)
SIGNING_KEY_ENV = "DJANGO_TOKEN_SIGNING_KEY"
VERIFYING_KEY_ENV = "DJANGO_TOKEN_VERIFYING_KEY"
PRIVATE_KEY_FILE = "jwt_private.pem"
PUBLIC_KEY_FILE = "jwt_public.pem"
KEYS_DIRECTORY = (
Path.home() / ".config" / "prowler-api"
) # `/home/prowler/.config/prowler-api` inside the container
_keys_initialized = False # Flag to prevent multiple executions within the same process
class ApiConfig(AppConfig):
@@ -6,7 +28,142 @@ class ApiConfig(AppConfig):
name = "api"
def ready(self):
from api import schema_extensions # noqa: F401
from api import signals # noqa: F401
from api.compliance import load_prowler_compliance
# Generate required cryptographic keys if not present, but only if:
# `"manage.py" not in sys.argv`: If an external server (e.g., Gunicorn) is running the app
# `os.environ.get("RUN_MAIN")`: If it's not a Django command or using `runserver`,
# only the main process will do it
if "manage.py" not in sys.argv or os.environ.get("RUN_MAIN"):
self._ensure_crypto_keys()
load_prowler_compliance()
def _ensure_crypto_keys(self):
"""
Orchestrator method that ensures all required cryptographic keys are present.
This method coordinates the generation of:
- RSA key pairs for JWT token signing and verification
Note: During development, Django spawns multiple processes (migrations, fixtures, etc.)
which will each generate their own keys. This is expected behavior and each process
will have consistent keys for its lifetime. In production, set the keys as environment
variables to avoid regeneration.
"""
global _keys_initialized
# Skip key generation if running tests
if hasattr(settings, "TESTING") and settings.TESTING:
return
# Skip if already initialized in this process
if _keys_initialized:
return
# Check if both JWT keys are set; if not, generate them
signing_key = env.str(SIGNING_KEY_ENV, default="").strip()
verifying_key = env.str(VERIFYING_KEY_ENV, default="").strip()
if not signing_key or not verifying_key:
logger.info(
f"Generating JWT RSA key pair. In production, set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' "
"environment variables."
)
self._ensure_jwt_keys()
# Mark as initialized to prevent future executions in this process
_keys_initialized = True
def _read_key_file(self, file_name):
"""
Utility method to read the contents of a file.
"""
file_path = KEYS_DIRECTORY / file_name
return file_path.read_text().strip() if file_path.is_file() else None
def _write_key_file(self, file_name, content, private=True):
"""
Utility method to write content to a file.
"""
try:
file_path = KEYS_DIRECTORY / file_name
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
file_path.chmod(0o600 if private else 0o644)
except Exception as e:
logger.error(
f"Error writing key file '{file_name}': {e}. "
f"Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
)
raise e
def _ensure_jwt_keys(self):
"""
Generate RSA key pairs for JWT token signing and verification
if they are not already set in environment variables.
"""
# Read existing keys from files if they exist
signing_key = self._read_key_file(PRIVATE_KEY_FILE)
verifying_key = self._read_key_file(PUBLIC_KEY_FILE)
if not signing_key or not verifying_key:
# Generate and store the RSA key pair
signing_key, verifying_key = self._generate_jwt_keys()
self._write_key_file(PRIVATE_KEY_FILE, signing_key, private=True)
self._write_key_file(PUBLIC_KEY_FILE, verifying_key, private=False)
logger.info("JWT keys generated and stored successfully")
else:
logger.info("JWT keys already generated")
# Set environment variables and Django settings
os.environ[SIGNING_KEY_ENV] = signing_key
settings.SIMPLE_JWT["SIGNING_KEY"] = signing_key
os.environ[VERIFYING_KEY_ENV] = verifying_key
settings.SIMPLE_JWT["VERIFYING_KEY"] = verifying_key
def _generate_jwt_keys(self):
"""
Generate and set RSA key pairs for JWT token operations.
"""
try:
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# Generate RSA key pair
private_key = rsa.generate_private_key( # Future improvement: we could read the next values from env vars
public_exponent=65537,
key_size=2048,
)
# Serialize private key (for signing)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
).decode("utf-8")
# Serialize public key (for verification)
public_key = private_key.public_key()
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
).decode("utf-8")
logger.debug("JWT RSA key pair generated successfully.")
return private_pem, public_pem
except ImportError as e:
logger.warning(
"The 'cryptography' package is required for automatic JWT key generation."
)
raise e
except Exception as e:
logger.error(
f"Error generating JWT keys: {e}. Please set '{SIGNING_KEY_ENV}' and '{VERIFYING_KEY_ENV}' manually."
)
raise e
+95
View File
@@ -0,0 +1,95 @@
from typing import Optional, Tuple
from uuid import UUID
from cryptography.fernet import InvalidToken
from django.utils import timezone
from drf_simple_apikey.backends import APIKeyAuthentication as BaseAPIKeyAuth
from drf_simple_apikey.crypto import get_crypto
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.request import Request
from rest_framework_simplejwt.authentication import JWTAuthentication
from api.db_router import MainRouter
from api.models import TenantAPIKey, TenantAPIKeyManager
class TenantAPIKeyAuthentication(BaseAPIKeyAuth):
model = TenantAPIKey
def __init__(self):
self.key_crypto = get_crypto()
def _authenticate_credentials(self, request, key):
"""
Override to use admin connection, bypassing RLS during authentication.
Delegates to parent after temporarily routing model queries to admin DB.
"""
# Temporarily point the model's manager to admin database
original_objects = self.model.objects
self.model.objects = self.model.objects.using(MainRouter.admin_db)
try:
# Call parent method which will now use admin database
return super()._authenticate_credentials(request, key)
finally:
# Restore original manager
self.model.objects = original_objects
def authenticate(self, request: Request):
prefixed_key = self.get_key(request)
# Split prefix from key (format: pk_xxxxxxxx.encrypted_key)
try:
prefix, key = prefixed_key.split(TenantAPIKeyManager.separator, 1)
except ValueError:
raise AuthenticationFailed("Invalid API Key.")
try:
entity, _ = self._authenticate_credentials(request, key)
except InvalidToken:
raise AuthenticationFailed("Invalid API Key.")
# Get the API key instance to update last_used_at and retrieve tenant info
# We need to decrypt again to get the pk (already validated by _authenticate_credentials)
payload = self.key_crypto.decrypt(key)
api_key_pk = payload["_pk"]
# Convert string UUID back to UUID object for lookup
if isinstance(api_key_pk, str):
api_key_pk = UUID(api_key_pk)
try:
api_key_instance = TenantAPIKey.objects.using(MainRouter.admin_db).get(
id=api_key_pk, prefix=prefix
)
except TenantAPIKey.DoesNotExist:
raise AuthenticationFailed("Invalid API Key.")
# Update last_used_at
api_key_instance.last_used_at = timezone.now()
api_key_instance.save(update_fields=["last_used_at"], using=MainRouter.admin_db)
return entity, {
"tenant_id": str(api_key_instance.tenant_id),
"sub": str(api_key_instance.entity.id),
"api_key_prefix": prefix,
}
class CombinedJWTOrAPIKeyAuthentication(BaseAuthentication):
jwt_auth = JWTAuthentication()
api_key_auth = TenantAPIKeyAuthentication()
def authenticate(self, request: Request) -> Optional[Tuple[object, dict]]:
auth_header = request.headers.get("Authorization", "")
# Prioritize JWT authentication if both are present
if auth_header.startswith("Bearer "):
return self.jwt_auth.authenticate(request)
if auth_header.startswith("Api-Key "):
return self.api_key_auth.authenticate(request)
# Default fallback
return self.jwt_auth.authenticate(request)
+85 -20
View File
@@ -1,13 +1,15 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from rest_framework import permissions
from rest_framework.exceptions import NotAuthenticated
from rest_framework.filters import SearchFilter
from rest_framework.permissions import SAFE_METHODS
from rest_framework_json_api import filters
from rest_framework_json_api.views import ModelViewSet
from rest_framework_simplejwt.authentication import JWTAuthentication
from api.db_router import MainRouter
from api.authentication import CombinedJWTOrAPIKeyAuthentication
from api.db_router import MainRouter, reset_read_db_alias, set_read_db_alias
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
from api.filters import CustomDjangoFilterBackend
from api.models import Role, Tenant
@@ -15,7 +17,7 @@ from api.rbac.permissions import HasPermissions
class BaseViewSet(ModelViewSet):
authentication_classes = [JWTAuthentication]
authentication_classes = [CombinedJWTOrAPIKeyAuthentication]
required_permissions = []
permission_classes = [permissions.IsAuthenticated, HasPermissions]
filter_backends = [
@@ -31,6 +33,20 @@ class BaseViewSet(ModelViewSet):
ordering_fields = "__all__"
ordering = ["id"]
def _get_request_db_alias(self, request):
if request is None:
return MainRouter.default_db
read_alias = (
MainRouter.replica_db
if request.method in SAFE_METHODS
and MainRouter.replica_db in settings.DATABASES
else None
)
if read_alias:
return read_alias
return MainRouter.default_db
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
@@ -48,8 +64,21 @@ class BaseViewSet(ModelViewSet):
class BaseRLSViewSet(BaseViewSet):
def dispatch(self, request, *args, **kwargs):
with transaction.atomic():
return super().dispatch(request, *args, **kwargs)
self.db_alias = self._get_request_db_alias(request)
alias_token = None
try:
if self.db_alias != MainRouter.default_db:
alias_token = set_read_db_alias(self.db_alias)
if request is not None:
request.db_alias = self.db_alias
with transaction.atomic(using=self.db_alias):
return super().dispatch(request, *args, **kwargs)
finally:
if alias_token is not None:
reset_read_db_alias(alias_token)
self.db_alias = MainRouter.default_db
def initial(self, request, *args, **kwargs):
# Ideally, this logic would be in the `.setup()` method but DRF view sets don't call it
@@ -61,7 +90,9 @@ class BaseRLSViewSet(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
with rls_transaction(
tenant_id, using=getattr(self, "db_alias", MainRouter.default_db)
):
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
@@ -73,18 +104,33 @@ class BaseRLSViewSet(BaseViewSet):
class BaseTenantViewset(BaseViewSet):
def dispatch(self, request, *args, **kwargs):
with transaction.atomic():
tenant = super().dispatch(request, *args, **kwargs)
self.db_alias = self._get_request_db_alias(request)
alias_token = None
try:
# If the request is a POST, create the admin role
if request.method == "POST":
isinstance(tenant, dict) and self._create_admin_role(tenant.data["id"])
except Exception as e:
self._handle_creation_error(e, tenant)
raise
if self.db_alias != MainRouter.default_db:
alias_token = set_read_db_alias(self.db_alias)
return tenant
if request is not None:
request.db_alias = self.db_alias
with transaction.atomic(using=self.db_alias):
tenant = super().dispatch(request, *args, **kwargs)
try:
# If the request is a POST, create the admin role
if request.method == "POST":
isinstance(tenant, dict) and self._create_admin_role(
tenant.data["id"]
)
except Exception as e:
self._handle_creation_error(e, tenant)
raise
return tenant
finally:
if alias_token is not None:
reset_read_db_alias(alias_token)
self.db_alias = MainRouter.default_db
def _create_admin_role(self, tenant_id):
Role.objects.using(MainRouter.admin_db).create(
@@ -117,14 +163,31 @@ class BaseTenantViewset(BaseViewSet):
raise NotAuthenticated("Tenant ID is not present in token")
user_id = str(request.user.id)
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
with rls_transaction(
value=user_id,
parameter=POSTGRES_USER_VAR,
using=getattr(self, "db_alias", MainRouter.default_db),
):
return super().initial(request, *args, **kwargs)
class BaseUserViewset(BaseViewSet):
def dispatch(self, request, *args, **kwargs):
with transaction.atomic():
return super().dispatch(request, *args, **kwargs)
self.db_alias = self._get_request_db_alias(request)
alias_token = None
try:
if self.db_alias != MainRouter.default_db:
alias_token = set_read_db_alias(self.db_alias)
if request is not None:
request.db_alias = self.db_alias
with transaction.atomic(using=self.db_alias):
return super().dispatch(request, *args, **kwargs)
finally:
if alias_token is not None:
reset_read_db_alias(alias_token)
self.db_alias = MainRouter.default_db
def initial(self, request, *args, **kwargs):
# TODO refactor after improving RLS on users
@@ -137,6 +200,8 @@ class BaseUserViewset(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
with rls_transaction(
tenant_id, using=getattr(self, "db_alias", MainRouter.default_db)
):
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
+11 -6
View File
@@ -150,12 +150,16 @@ def generate_scan_compliance(
requirement["checks"][check_id] = status
requirement["checks_status"][status.lower()] += 1
if requirement["status"] != "FAIL" and any(
value == "FAIL" for value in requirement["checks"].values()
):
requirement["status"] = "FAIL"
compliance_overview[compliance_id]["requirements_status"]["passed"] -= 1
compliance_overview[compliance_id]["requirements_status"]["failed"] += 1
if requirement["status"] != "FAIL" and any(
value == "FAIL" for value in requirement["checks"].values()
):
requirement["status"] = "FAIL"
compliance_overview[compliance_id]["requirements_status"][
"passed"
] -= 1
compliance_overview[compliance_id]["requirements_status"][
"failed"
] += 1
def generate_compliance_overview_template(prowler_compliance: dict):
@@ -225,6 +229,7 @@ def generate_compliance_overview_template(prowler_compliance: dict):
# Build compliance dictionary
compliance_dict = {
"framework": compliance_data.Framework,
"name": compliance_data.Name,
"version": compliance_data.Version,
"provider": provider_type,
"description": compliance_data.Description,
+30
View File
@@ -1,9 +1,31 @@
from contextvars import ContextVar
from django.conf import settings
ALLOWED_APPS = ("django", "socialaccount", "account", "authtoken", "silk")
_read_db_alias = ContextVar("read_db_alias", default=None)
def set_read_db_alias(alias: str | None):
if not alias:
return None
return _read_db_alias.set(alias)
def get_read_db_alias() -> str | None:
return _read_db_alias.get()
def reset_read_db_alias(token) -> None:
if token is not None:
_read_db_alias.reset(token)
class MainRouter:
default_db = "default"
admin_db = "admin"
replica_db = "replica"
def db_for_read(self, model, **hints): # noqa: F841
model_table_name = model._meta.db_table
@@ -11,6 +33,9 @@ class MainRouter:
model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS
):
return self.admin_db
read_alias = get_read_db_alias()
if read_alias:
return read_alias
return None
def db_for_write(self, model, **hints): # noqa: F841
@@ -27,3 +52,8 @@ class MainRouter:
if {obj1._state.db, obj2._state.db} <= {self.default_db, self.admin_db}:
return True
return None
READ_REPLICA_ALIAS = (
MainRouter.replica_db if MainRouter.replica_db in settings.DATABASES else None
)
+74 -11
View File
@@ -6,12 +6,14 @@ from datetime import datetime, timedelta, timezone
from django.conf import settings
from django.contrib.auth.models import BaseUserManager
from django.db import connection, models, transaction
from django.db import DEFAULT_DB_ALIAS, connection, connections, models, transaction
from django_celery_beat.models import PeriodicTask
from psycopg2 import connect as psycopg2_connect
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
from rest_framework_json_api.serializers import ValidationError
from api.db_router import get_read_db_alias, reset_read_db_alias, set_read_db_alias
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
DB_PASSWORD = (
settings.DATABASES["default"]["PASSWORD"] if not settings.TESTING else "test"
@@ -49,7 +51,11 @@ def psycopg_connection(database_alias: str):
@contextmanager
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
def rls_transaction(
value: str,
parameter: str = POSTGRES_TENANT_VAR,
using: str | None = None,
):
"""
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
if the value is a valid UUID.
@@ -57,16 +63,32 @@ def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
Args:
value (str): Database configuration parameter value.
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
using (str | None): Optional database alias to run the transaction against. Defaults to the
active read alias (if any) or Django's default connection.
"""
with transaction.atomic():
with connection.cursor() as cursor:
try:
# just in case the value is an UUID object
uuid.UUID(str(value))
except ValueError:
raise ValidationError("Must be a valid UUID")
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
yield cursor
requested_alias = using or get_read_db_alias()
db_alias = requested_alias or DEFAULT_DB_ALIAS
if db_alias not in connections:
db_alias = DEFAULT_DB_ALIAS
router_token = None
try:
if db_alias != DEFAULT_DB_ALIAS:
router_token = set_read_db_alias(db_alias)
with transaction.atomic(using=db_alias):
conn = connections[db_alias]
with conn.cursor() as cursor:
try:
# just in case the value is a UUID object
uuid.UUID(str(value))
except ValueError:
raise ValidationError("Must be a valid UUID")
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
yield cursor
finally:
if router_token is not None:
reset_read_db_alias(router_token)
class CustomUserManager(BaseUserManager):
@@ -175,6 +197,29 @@ def create_objects_in_batches(
model.objects.bulk_create(chunk, batch_size)
def update_objects_in_batches(
tenant_id: str, model, objects: list, fields: list, batch_size: int = 500
):
"""
Bulk-update model instances in repeated, per-tenant RLS transactions.
All chunks execute in their own transaction, so no single transaction
grows too large.
Args:
tenant_id (str): UUID string of the tenant under which to set RLS.
model: Django model class whose `.objects.bulk_update()` will be called.
objects (list): List of model instances (saved) to bulk-update.
fields (list): List of field names to update.
batch_size (int): Maximum number of objects per bulk_update call.
"""
total = len(objects)
for start in range(0, total, batch_size):
chunk = objects[start : start + batch_size]
with rls_transaction(value=tenant_id, parameter=POSTGRES_TENANT_VAR):
model.objects.bulk_update(chunk, fields, batch_size)
# Postgres Enums
@@ -411,6 +456,12 @@ def drop_index_on_partitions(
schema_editor.execute(sql)
def generate_api_key_prefix():
"""Generate a random 8-character prefix for API keys (e.g., 'pk_abc123de')."""
random_chars = generate_random_token(length=8)
return f"pk_{random_chars}"
# Postgres enum definition for member role
@@ -529,3 +580,15 @@ class IntegrationTypeEnum(EnumType):
class IntegrationTypeEnumField(PostgresEnumField):
def __init__(self, *args, **kwargs):
super().__init__("integration_type", *args, **kwargs)
# Postgres enum definition for Processor type
class ProcessorTypeEnum(EnumType):
enum_type_name = "processor_type"
class ProcessorTypeEnumField(PostgresEnumField):
def __init__(self, *args, **kwargs):
super().__init__("processor_type", *args, **kwargs)
+40 -10
View File
@@ -1,6 +1,10 @@
from django.core.exceptions import ValidationError as django_validation_error
from rest_framework import status
from rest_framework.exceptions import APIException
from rest_framework.exceptions import (
APIException,
AuthenticationFailed,
NotAuthenticated,
)
from rest_framework_json_api.exceptions import exception_handler
from rest_framework_json_api.serializers import ValidationError
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
@@ -57,19 +61,45 @@ class TaskInProgressException(TaskManagementError):
super().__init__()
# Provider connection errors
class ProviderConnectionError(Exception):
"""Base exception for provider connection errors."""
def custom_exception_handler(exc, context):
if isinstance(exc, django_validation_error):
if hasattr(exc, "error_dict"):
exc = ValidationError(exc.message_dict)
else:
exc = ValidationError(detail=exc.messages[0], code=exc.code)
elif isinstance(exc, (TokenError, InvalidToken)):
if (
hasattr(exc, "detail")
and isinstance(exc.detail, dict)
and "messages" in exc.detail
):
exc.detail["messages"] = [
message_item["message"] for message_item in exc.detail["messages"]
]
# Force 401 status for AuthenticationFailed exceptions regardless of the authentication backend
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated, TokenError)):
exc.status_code = status.HTTP_401_UNAUTHORIZED
if isinstance(exc, (TokenError, InvalidToken)):
if (
hasattr(exc, "detail")
and isinstance(exc.detail, dict)
and "messages" in exc.detail
):
exc.detail["messages"] = [
message_item["message"] for message_item in exc.detail["messages"]
]
return exception_handler(exc, context)
class ConflictException(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = "A conflict occurred. The resource already exists."
default_code = "conflict"
def __init__(self, detail=None, code=None, pointer=None):
error_detail = {
"detail": detail or self.default_detail,
"status": self.status_code,
"code": self.default_code,
}
if pointer:
error_detail["source"] = {"pointer": pointer}
super().__init__(detail=[error_detail])
+229 -6
View File
@@ -1,7 +1,8 @@
from datetime import date, datetime, timedelta, timezone
from dateutil.parser import parse
from django.conf import settings
from django.db.models import Q
from django.db.models import F, Q
from django_filters.rest_framework import (
BaseInFilter,
BooleanFilter,
@@ -27,7 +28,9 @@ from api.models import (
Integration,
Invitation,
Membership,
OverviewStatusChoices,
PermissionChoices,
Processor,
Provider,
ProviderGroup,
ProviderSecret,
@@ -40,6 +43,7 @@ from api.models import (
StateChoices,
StatusChoices,
Task,
TenantAPIKey,
User,
)
from api.rls import Tenant
@@ -216,10 +220,31 @@ class MembershipFilter(FilterSet):
class ProviderFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
connected = BooleanFilter()
inserted_at = DateFilter(
field_name="inserted_at",
lookup_expr="date",
help_text="""Filter by date when the provider was added
(format: YYYY-MM-DD)""",
)
updated_at = DateFilter(
field_name="updated_at",
lookup_expr="date",
help_text="""Filter by date when the provider was updated
(format: YYYY-MM-DD)""",
)
connected = BooleanFilter(
help_text="""Filter by connection status. Set to True to return only
connected providers, or False to return only providers with failed
connections. If not specified, both connected and failed providers are
included. Providers with no connection attempt (status is null) are
excluded from this filter."""
)
provider = ChoiceFilter(choices=Provider.ProviderChoices.choices)
provider__in = ChoiceInFilter(
field_name="provider",
choices=Provider.ProviderChoices.choices,
lookup_expr="in",
)
class Meta:
model = Provider
@@ -338,6 +363,8 @@ class ResourceFilter(ProviderRelationshipFilterSet):
tags = CharFilter(method="filter_tag")
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
scan = UUIDFilter(field_name="provider__scan", lookup_expr="exact")
scan__in = UUIDInFilter(field_name="provider__scan", lookup_expr="in")
class Meta:
model = Resource
@@ -352,6 +379,82 @@ class ResourceFilter(ProviderRelationshipFilterSet):
"updated_at": ["gte", "lte"],
}
def filter_queryset(self, queryset):
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
self.data.get("updated_at")
or self.data.get("updated_at__date")
or self.data.get("updated_at__gte")
or self.data.get("updated_at__lte")
):
raise ValidationError(
[
{
"detail": "At least one date filter is required: filter[updated_at], filter[updated_at.gte], "
"or filter[updated_at.lte].",
"status": 400,
"source": {"pointer": "/data/attributes/updated_at"},
"code": "required",
}
]
)
gte_date = (
parse(self.data.get("updated_at__gte")).date()
if self.data.get("updated_at__gte")
else datetime.now(timezone.utc).date()
)
lte_date = (
parse(self.data.get("updated_at__lte")).date()
if self.data.get("updated_at__lte")
else datetime.now(timezone.utc).date()
)
if abs(lte_date - gte_date) > timedelta(
days=settings.FINDINGS_MAX_DAYS_IN_RANGE
):
raise ValidationError(
[
{
"detail": f"The date range cannot exceed {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
"status": 400,
"source": {"pointer": "/data/attributes/updated_at"},
"code": "invalid",
}
]
)
return super().filter_queryset(queryset)
def filter_tag_key(self, queryset, name, value):
return queryset.filter(Q(tags__key=value) | Q(tags__key__icontains=value))
def filter_tag_value(self, queryset, name, value):
return queryset.filter(Q(tags__value=value) | Q(tags__value__icontains=value))
def filter_tag(self, queryset, name, value):
# We won't know what the user wants to filter on just based on the value,
# and we don't want to build special filtering logic for every possible
# provider tag spec, so we'll just do a full text search
return queryset.filter(tags__text_search=value)
class LatestResourceFilter(ProviderRelationshipFilterSet):
tag_key = CharFilter(method="filter_tag_key")
tag_value = CharFilter(method="filter_tag_value")
tag = CharFilter(method="filter_tag")
tags = CharFilter(method="filter_tag")
class Meta:
model = Resource
fields = {
"provider": ["exact", "in"],
"uid": ["exact", "icontains"],
"name": ["exact", "icontains"],
"region": ["exact", "icontains", "in"],
"service": ["exact", "icontains", "in"],
"type": ["exact", "icontains", "in"],
}
def filter_tag_key(self, queryset, name, value):
return queryset.filter(Q(tags__key=value) | Q(tags__key__icontains=value))
@@ -567,8 +670,16 @@ class LatestFindingFilter(CommonFindingFilters):
class ProviderSecretFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
inserted_at = DateFilter(
field_name="inserted_at",
lookup_expr="date",
help_text="Filter by date when the secret was added (format: YYYY-MM-DD)",
)
updated_at = DateFilter(
field_name="updated_at",
lookup_expr="date",
help_text="Filter by date when the secret was updated (format: YYYY-MM-DD)",
)
provider = UUIDFilter(field_name="provider__id", lookup_expr="exact")
class Meta:
@@ -670,6 +781,72 @@ class ScanSummaryFilter(FilterSet):
}
class ScanSummarySeverityFilter(ScanSummaryFilter):
"""Filter for findings_severity ScanSummary endpoint - includes status filters"""
# Custom status filters - only for severity grouping endpoint
status = ChoiceFilter(method="filter_status", choices=OverviewStatusChoices.choices)
status__in = CharInFilter(method="filter_status_in", lookup_expr="in")
def filter_status(self, queryset, name, value):
# Validate the status value
if value not in [choice[0] for choice in OverviewStatusChoices.choices]:
raise ValidationError(f"Invalid status value: {value}")
# Apply the filter by annotating the queryset with the status field
if value == OverviewStatusChoices.FAIL:
return queryset.annotate(status_count=F("fail"))
elif value == OverviewStatusChoices.PASS:
return queryset.annotate(status_count=F("_pass"))
else:
return queryset.annotate(status_count=F("total"))
def filter_status_in(self, queryset, name, value):
# Validate the status values
valid_statuses = [choice[0] for choice in OverviewStatusChoices.choices]
for status_val in value:
if status_val not in valid_statuses:
raise ValidationError(f"Invalid status value: {status_val}")
# If all statuses or no valid statuses, use total
if (
set(value)
>= {
OverviewStatusChoices.FAIL,
OverviewStatusChoices.PASS,
}
or not value
):
return queryset.annotate(status_count=F("total"))
# Build the sum expression based on status values
sum_expression = None
for status in value:
if status == OverviewStatusChoices.FAIL:
field_expr = F("fail")
elif status == OverviewStatusChoices.PASS:
field_expr = F("_pass")
else:
continue
if sum_expression is None:
sum_expression = field_expr
else:
sum_expression = sum_expression + field_expr
if sum_expression is None:
return queryset.annotate(status_count=F("total"))
return queryset.annotate(status_count=sum_expression)
class Meta:
model = ScanSummary
fields = {
"inserted_at": ["date", "gte", "lte"],
"region": ["exact", "icontains", "in"],
}
class ServiceOverviewFilter(ScanSummaryFilter):
def is_valid(self):
# Check if at least one of the inserted_at filters is present
@@ -704,3 +881,49 @@ class IntegrationFilter(FilterSet):
fields = {
"inserted_at": ["date", "gte", "lte"],
}
class ProcessorFilter(FilterSet):
processor_type = ChoiceFilter(choices=Processor.ProcessorChoices.choices)
processor_type__in = ChoiceInFilter(
choices=Processor.ProcessorChoices.choices,
field_name="processor_type",
lookup_expr="in",
)
class IntegrationJiraFindingsFilter(FilterSet):
# To be expanded as needed
finding_id = UUIDFilter(field_name="id", lookup_expr="exact")
finding_id__in = UUIDInFilter(field_name="id", lookup_expr="in")
class Meta:
model = Finding
fields = {}
def filter_queryset(self, queryset):
# Validate that there is at least one filter provided
if not self.data:
raise ValidationError(
{
"findings": "No finding filters provided. At least one filter is required."
}
)
return super().filter_queryset(queryset)
class TenantApiKeyFilter(FilterSet):
inserted_at = DateFilter(field_name="created", lookup_expr="date")
inserted_at__gte = DateFilter(field_name="created", lookup_expr="gte")
inserted_at__lte = DateFilter(field_name="created", lookup_expr="lte")
expires_at = DateFilter(field_name="expiry_date", lookup_expr="date")
expires_at__gte = DateFilter(field_name="expiry_date", lookup_expr="gte")
expires_at__lte = DateFilter(field_name="expiry_date", lookup_expr="lte")
class Meta:
model = TenantAPIKey
fields = {
"prefix": ["exact", "icontains"],
"revoked": ["exact"],
"name": ["exact", "icontains"],
}
@@ -3,7 +3,7 @@
"model": "api.user",
"pk": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
"fields": {
"password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=",
"password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=",
"last_login": null,
"name": "Devie Prowlerson",
"email": "dev@prowler.com",
@@ -16,7 +16,7 @@
"model": "api.user",
"pk": "b6493a3a-c997-489b-8b99-278bf74de9f6",
"fields": {
"password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=",
"password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=",
"last_login": null,
"name": "Devietoo Prowlerson",
"email": "dev2@prowler.com",
@@ -24,5 +24,18 @@
"is_active": true,
"date_joined": "2024-09-18T09:04:20.850Z"
}
},
{
"model": "api.user",
"pk": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f",
"fields": {
"password": "pbkdf2_sha256$870000$Z63pGJ7nre48hfcGbk5S0O$rQpKczAmijs96xa+gPVJifpT3Fetb8DOusl5Eq6gxac=",
"last_login": null,
"name": "E2E Test User",
"email": "e2e@prowler.com",
"company_name": "Prowler E2E Tests",
"is_active": true,
"date_joined": "2024-01-01T00:00:00.850Z"
}
}
]
@@ -46,5 +46,24 @@
"role": "member",
"date_joined": "2024-09-19T11:03:59.712Z"
}
},
{
"model": "api.tenant",
"pk": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b",
"fields": {
"inserted_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"name": "E2E Test Tenant"
}
},
{
"model": "api.membership",
"pk": "9b1a2c3d-4e5f-6789-abc1-23456789def0",
"fields": {
"user": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f",
"tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b",
"role": "owner",
"date_joined": "2024-01-01T00:00:00.000Z"
}
}
]
@@ -149,5 +149,32 @@
"user": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
"inserted_at": "2024-11-20T15:36:14.302Z"
}
},
{
"model": "api.role",
"pk": "a5b6c7d8-9e0f-1234-5678-90abcdef1234",
"fields": {
"tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b",
"name": "e2e_admin",
"manage_users": true,
"manage_account": true,
"manage_billing": true,
"manage_providers": true,
"manage_integrations": true,
"manage_scans": true,
"unlimited_visibility": true,
"inserted_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
},
{
"model": "api.userrolerelationship",
"pk": "f1e2d3c4-b5a6-9876-5432-10fedcba9876",
"fields": {
"tenant": "7c8f94a3-e2d1-4b3a-9f87-2c4d5e6f1a2b",
"role": "a5b6c7d8-9e0f-1234-5678-90abcdef1234",
"user": "6d4f8a91-3c2e-4b5a-8f7d-1e9c5b2a4d6f",
"inserted_at": "2024-01-01T00:00:00.000Z"
}
}
]
File diff suppressed because one or more lines are too long
+8 -2
View File
@@ -8,9 +8,14 @@ def extract_auth_info(request) -> dict:
if getattr(request, "auth", None) is not None:
tenant_id = request.auth.get("tenant_id", "N/A")
user_id = request.auth.get("sub", "N/A")
api_key_prefix = request.auth.get("api_key_prefix", "N/A")
else:
tenant_id, user_id = "N/A", "N/A"
return {"tenant_id": tenant_id, "user_id": user_id}
tenant_id, user_id, api_key_prefix = "N/A", "N/A", "N/A"
return {
"tenant_id": tenant_id,
"user_id": user_id,
"api_key_prefix": api_key_prefix,
}
class APILoggingMiddleware:
@@ -38,6 +43,7 @@ class APILoggingMiddleware:
extra={
"user_id": auth_info["user_id"],
"tenant_id": auth_info["tenant_id"],
"api_key_prefix": auth_info["api_key_prefix"],
"method": request.method,
"path": request.path,
"query_params": request.GET.dict(),
@@ -11,7 +11,7 @@ import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0030_samlconfigurations"),
("api", "0029_findings_check_index_parent"),
]
operations = [
@@ -54,6 +54,7 @@ class Migration(migrations.Migration):
("gpt-4o-mini-2024-07-18", "GPT-4o Mini v2024-07-18"),
("gpt-4o-mini", "GPT-4o Mini Default"),
],
default="gpt-4o-2024-08-06",
help_text="Must be one of the supported model names",
max_length=50,
),
@@ -0,0 +1,24 @@
# Generated by Django 5.1.10 on 2025-06-23 10:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0030_lighthouseconfiguration"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
migrations.AlterField(
model_name="scan",
name="scheduler_task",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="django_celery_beat.periodictask",
),
),
]
@@ -1,57 +1,61 @@
# Generated by Django 5.1.8 on 2025-05-15 09:54
# Generated by Django 5.1.10 on 2025-07-02 15:47
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import api.db_utils
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0029_findings_check_index_parent"),
("api", "0031_scan_disable_on_cascade_periodic_tasks"),
]
operations = [
migrations.AlterField(
model_name="integration",
name="integration_type",
field=api.db_utils.IntegrationTypeEnumField(
choices=[
("amazon_s3", "Amazon S3"),
("aws_security_hub", "AWS Security Hub"),
("jira", "JIRA"),
("slack", "Slack"),
]
),
),
migrations.CreateModel(
name="SAMLDomainIndex",
name="SAMLToken",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("email_domain", models.CharField(max_length=254, unique=True)),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("expires_at", models.DateTimeField(editable=False)),
("token", models.JSONField(unique=True)),
(
"tenant",
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "saml_domain_index",
"db_table": "saml_tokens",
},
),
migrations.AddConstraint(
model_name="samldomainindex",
constraint=models.UniqueConstraint(
fields=("email_domain", "tenant"),
name="unique_resources_by_email_domain",
),
),
migrations.AddConstraint(
model_name="samldomainindex",
constraint=api.rls.BaseSecurityConstraint(
name="statements_on_samldomainindex",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.CreateModel(
name="SAMLConfiguration",
fields=[
@@ -105,16 +109,42 @@ class Migration(migrations.Migration):
fields=("tenant",), name="unique_samlconfig_per_tenant"
),
),
migrations.AlterField(
model_name="integration",
name="integration_type",
field=api.db_utils.IntegrationTypeEnumField(
choices=[
("amazon_s3", "Amazon S3"),
("aws_security_hub", "AWS Security Hub"),
("jira", "JIRA"),
("slack", "Slack"),
]
migrations.CreateModel(
name="SAMLDomainIndex",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("email_domain", models.CharField(max_length=254, unique=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "saml_domain_index",
},
),
migrations.AddConstraint(
model_name="samldomainindex",
constraint=models.UniqueConstraint(
fields=("email_domain", "tenant"),
name="unique_resources_by_email_domain",
),
),
migrations.AddConstraint(
model_name="samldomainindex",
constraint=api.rls.BaseSecurityConstraint(
name="statements_on_samldomainindex",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
@@ -0,0 +1,34 @@
# Generated by Django 5.1.5 on 2025-03-03 15:46
from functools import partial
from django.db import migrations
from api.db_utils import PostgresEnumMigration, ProcessorTypeEnum, register_enum
from api.models import Processor
ProcessorTypeEnumMigration = PostgresEnumMigration(
enum_name="processor_type",
enum_values=tuple(
processor_type[0] for processor_type in Processor.ProcessorChoices.choices
),
)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0032_saml"),
]
operations = [
migrations.RunPython(
ProcessorTypeEnumMigration.create_enum_type,
reverse_code=ProcessorTypeEnumMigration.drop_enum_type,
),
migrations.RunPython(
partial(register_enum, enum_class=ProcessorTypeEnum),
reverse_code=migrations.RunPython.noop,
),
]
@@ -0,0 +1,88 @@
# Generated by Django 5.1.5 on 2025-03-26 13:04
import uuid
import django.db.models.deletion
from django.db import migrations, models
import api.db_utils
import api.rls
from api.rls import RowLevelSecurityConstraint
class Migration(migrations.Migration):
dependencies = [
("api", "0033_processors_enum"),
]
operations = [
migrations.CreateModel(
name="Processor",
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)),
(
"processor_type",
api.db_utils.ProcessorTypeEnumField(
choices=[("mutelist", "Mutelist")]
),
),
("configuration", models.JSONField(default=dict)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "processors",
"abstract": False,
"indexes": [
models.Index(
fields=["tenant_id", "id"], name="processor_tenant_id_idx"
),
models.Index(
fields=["tenant_id", "processor_type"],
name="processor_tenant_type_idx",
),
],
},
),
migrations.AddConstraint(
model_name="processor",
constraint=models.UniqueConstraint(
fields=("tenant_id", "processor_type"),
name="unique_processor_types_tenant",
),
),
migrations.AddConstraint(
model_name="processor",
constraint=RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_processor",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.AddField(
model_name="scan",
name="processor",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="scans",
related_query_name="scan",
to="api.processor",
),
),
]
@@ -0,0 +1,22 @@
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0034_processors"),
]
operations = [
migrations.AddField(
model_name="finding",
name="muted_reason",
field=models.TextField(
blank=True,
max_length=500,
null=True,
validators=[django.core.validators.MinLengthValidator(3)],
),
),
]
@@ -0,0 +1,30 @@
from functools import partial
from django.db import migrations
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0035_finding_muted_reason"),
]
operations = [
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_finding_idx",
columns="tenant_id, finding_id",
method="BTREE",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_finding_idx",
),
),
]
@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0036_rfm_tenant_finding_index_partitions"),
]
operations = [
migrations.AddIndex(
model_name="resourcefindingmapping",
index=models.Index(
fields=["tenant_id", "finding_id"],
name="rfm_tenant_finding_idx",
),
),
]
@@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0037_rfm_tenant_finding_index_parent"),
]
operations = [
migrations.AddField(
model_name="resource",
name="failed_findings_count",
field=models.IntegerField(default=0),
)
]
@@ -0,0 +1,20 @@
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0038_resource_failed_findings_count"),
]
operations = [
AddIndexConcurrently(
model_name="resource",
index=models.Index(
fields=["tenant_id", "-failed_findings_count", "id"],
name="resources_failed_findings_idx",
),
),
]
@@ -0,0 +1,30 @@
from functools import partial
from django.db import migrations
from api.db_utils import create_index_on_partitions, drop_index_on_partitions
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0039_resource_resources_failed_findings_idx"),
]
operations = [
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_resource_idx",
columns="tenant_id, resource_id",
method="BTREE",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="resource_finding_mappings",
index_name="rfm_tenant_resource_idx",
),
),
]
@@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0040_rfm_tenant_resource_index_partitions"),
]
operations = [
migrations.AddIndex(
model_name="resourcefindingmapping",
index=models.Index(
fields=["tenant_id", "resource_id"],
name="rfm_tenant_resource_idx",
),
),
]
@@ -0,0 +1,23 @@
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0041_rfm_tenant_resource_parent_partitions"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
AddIndexConcurrently(
model_name="scan",
index=models.Index(
condition=models.Q(("state", "completed")),
fields=["tenant_id", "provider_id", "-inserted_at"],
include=("id",),
name="scans_prov_ins_desc_idx",
),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.1.7 on 2025-07-09 14:44
from django.db import migrations
import api.db_utils
class Migration(migrations.Migration):
dependencies = [
("api", "0042_scan_scans_prov_ins_desc_idx"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="provider",
field=api.db_utils.ProviderEnumField(
choices=[
("aws", "AWS"),
("azure", "Azure"),
("gcp", "GCP"),
("kubernetes", "Kubernetes"),
("m365", "M365"),
("github", "GitHub"),
],
default="aws",
),
),
migrations.RunSQL(
"ALTER TYPE provider ADD VALUE IF NOT EXISTS 'github';",
reverse_sql=migrations.RunSQL.noop,
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.10 on 2025-07-17 11:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0043_github_provider"),
]
operations = [
migrations.AddConstraint(
model_name="integration",
constraint=models.UniqueConstraint(
fields=("configuration", "tenant"),
name="unique_configuration_per_tenant",
),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.10 on 2025-07-21 16:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0044_integration_unique_configuration_per_tenant"),
]
operations = [
migrations.AlterField(
model_name="scan",
name="output_location",
field=models.CharField(blank=True, max_length=4096, null=True),
),
]
@@ -0,0 +1,33 @@
# Generated by Django 5.1.10 on 2025-08-20 09:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0045_alter_scan_output_location"),
]
operations = [
migrations.AlterField(
model_name="lighthouseconfiguration",
name="model",
field=models.CharField(
choices=[
("gpt-4o-2024-11-20", "GPT-4o v2024-11-20"),
("gpt-4o-2024-08-06", "GPT-4o v2024-08-06"),
("gpt-4o-2024-05-13", "GPT-4o v2024-05-13"),
("gpt-4o", "GPT-4o Default"),
("gpt-4o-mini-2024-07-18", "GPT-4o Mini v2024-07-18"),
("gpt-4o-mini", "GPT-4o Mini Default"),
("gpt-5-2025-08-07", "GPT-5 v2025-08-07"),
("gpt-5", "GPT-5 Default"),
("gpt-5-mini-2025-08-07", "GPT-5 Mini v2025-08-07"),
("gpt-5-mini", "GPT-5 Mini Default"),
],
default="gpt-4o-2024-08-06",
help_text="Must be one of the supported model names",
max_length=50,
),
),
]
@@ -0,0 +1,16 @@
# Generated by Django 5.1.10 on 2025-08-20 08:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("api", "0046_lighthouse_gpt5"),
]
operations = [
migrations.RemoveConstraint(
model_name="integration",
name="unique_configuration_per_tenant",
),
]
@@ -0,0 +1,136 @@
# Generated by Django 5.1.12 on 2025-09-30 13:10
import uuid
import django.core.validators
import django.db.models.deletion
import drf_simple_apikey.models
from django.conf import settings
from django.db import migrations, models
import api.db_utils
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0047_remove_integration_unique_configuration_per_tenant"),
]
operations = [
migrations.CreateModel(
name="TenantAPIKey",
fields=[
(
"name",
models.CharField(
max_length=255,
validators=[django.core.validators.MinLengthValidator(3)],
),
),
(
"expiry_date",
models.DateTimeField(
default=drf_simple_apikey.models._expiry_date,
help_text="Once API key expires, entities cannot use it anymore.",
verbose_name="Expires",
),
),
(
"revoked",
models.BooleanField(
blank=True,
default=False,
help_text="If the API key is revoked, entities cannot use it anymore. (This cannot be undone.)",
),
),
("created", models.DateTimeField(auto_now=True)),
(
"whitelisted_ips",
models.JSONField(
blank=True,
help_text="List of allowed IP addresses for this API key.",
null=True,
),
),
(
"blacklisted_ips",
models.JSONField(
blank=True,
help_text="List of denied IP addresses for this API key.",
null=True,
),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"prefix",
models.CharField(
default=api.db_utils.generate_api_key_prefix,
editable=False,
help_text="Unique prefix to identify the API key",
max_length=11,
unique=True,
),
),
(
"last_used_at",
models.DateTimeField(
blank=True,
help_text="Last time this API key was used for authentication",
null=True,
),
),
(
"entity",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="user_api_keys",
to=settings.AUTH_USER_MODEL,
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "api_keys",
"abstract": False,
"indexes": [
models.Index(
fields=["tenant_id", "prefix"],
name="api_keys_tenant_prefix_idx",
)
],
"constraints": [
models.UniqueConstraint(
fields=("tenant_id", "prefix"), name="unique_api_key_prefixes"
),
models.UniqueConstraint(
fields=("tenant_id", "name"),
name="unique_api_key_name_per_tenant",
),
],
},
),
migrations.AddConstraint(
model_name="tenantapikey",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_tenantapikey",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
@@ -0,0 +1,21 @@
# Generated by Django 5.1.12 on 2025-10-07 10:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0048_api_key"),
]
operations = [
migrations.AddField(
model_name="compliancerequirementoverview",
name="passed_findings",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="compliancerequirementoverview",
name="total_findings",
field=models.IntegerField(default=0),
),
]
+251 -27
View File
@@ -2,6 +2,7 @@ import json
import logging
import re
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4
from allauth.socialaccount.models import SocialApp
@@ -21,6 +22,8 @@ from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
from django_celery_results.models import TaskResult
from drf_simple_apikey.crypto import get_crypto
from drf_simple_apikey.models import AbstractAPIKey, AbstractAPIKeyManager
from psqlextra.manager import PostgresManager
from psqlextra.models import PostgresPartitionedModel
from psqlextra.types import PostgresPartitioningMethod
@@ -33,6 +36,7 @@ from api.db_utils import (
IntegrationTypeEnumField,
InvitationStateEnumField,
MemberRoleEnumField,
ProcessorTypeEnumField,
ProviderEnumField,
ProviderSecretTypeEnumField,
ScanTriggerEnumField,
@@ -40,6 +44,7 @@ from api.db_utils import (
StateEnumField,
StatusEnumField,
enum_to_choices,
generate_api_key_prefix,
generate_random_token,
one_week_from_now,
)
@@ -72,6 +77,15 @@ class StatusChoices(models.TextChoices):
MANUAL = "MANUAL", _("Manual")
class OverviewStatusChoices(models.TextChoices):
"""
Status filters allowed in overview/severity endpoints.
"""
FAIL = "FAIL", _("Fail")
PASS = "PASS", _("Pass")
class StateChoices(models.TextChoices):
AVAILABLE = "available", _("Available")
SCHEDULED = "scheduled", _("Scheduled")
@@ -114,6 +128,17 @@ class ActiveProviderPartitionedManager(PostgresManager, ActiveProviderManager):
return super().get_queryset().filter(self.active_provider_filter())
class TenantAPIKeyManager(AbstractAPIKeyManager):
separator = "."
def assign_api_key(self, obj) -> str:
payload = {"_pk": str(obj.pk), "_exp": obj.expiry_date.timestamp()}
key = get_crypto().generate(payload)
prefixed_key = f"{obj.prefix}{self.separator}{key}"
return prefixed_key
class User(AbstractBaseUser):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=150, validators=[MinLengthValidator(3)])
@@ -193,6 +218,60 @@ class Membership(models.Model):
resource_name = "memberships"
class TenantAPIKey(AbstractAPIKey, RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=100, validators=[MinLengthValidator(3)])
prefix = models.CharField(
max_length=11,
unique=True,
default=generate_api_key_prefix,
editable=False,
help_text="Unique prefix to identify the API key",
)
last_used_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last time this API key was used for authentication",
)
entity = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="user_api_keys",
)
objects = TenantAPIKeyManager()
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "api_keys"
constraints = [
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
models.UniqueConstraint(
fields=("tenant_id", "prefix"),
name="unique_api_key_prefixes",
),
models.UniqueConstraint(
fields=("tenant_id", "name"),
name="unique_api_key_name_per_tenant",
),
]
indexes = [
models.Index(
fields=["tenant_id", "prefix"], name="api_keys_tenant_prefix_idx"
),
]
class JSONAPIMeta:
resource_name = "api-keys"
class Provider(RowLevelSecurityProtectedModel):
objects = ActiveProviderManager()
all_objects = models.Manager()
@@ -203,6 +282,7 @@ class Provider(RowLevelSecurityProtectedModel):
GCP = "gcp", _("GCP")
KUBERNETES = "kubernetes", _("Kubernetes")
M365 = "m365", _("M365")
GITHUB = "github", _("GitHub")
@staticmethod
def validate_aws_uid(value):
@@ -263,6 +343,16 @@ class Provider(RowLevelSecurityProtectedModel):
pointer="/data/attributes/uid",
)
@staticmethod
def validate_github_uid(value):
if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9-]{0,38}$", value):
raise ModelValidationError(
detail="GitHub provider ID must be a valid GitHub username or organization name (1-39 characters, "
"starting with alphanumeric, containing only alphanumeric characters and hyphens).",
code="github-uid",
pointer="/data/attributes/uid",
)
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)
@@ -408,20 +498,6 @@ class Scan(RowLevelSecurityProtectedModel):
name = models.CharField(
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
)
provider = models.ForeignKey(
Provider,
on_delete=models.CASCADE,
related_name="scans",
related_query_name="scan",
)
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="scans",
related_query_name="scan",
null=True,
blank=True,
)
trigger = ScanTriggerEnumField(
choices=TriggerChoices.choices,
)
@@ -437,11 +513,31 @@ class Scan(RowLevelSecurityProtectedModel):
completed_at = models.DateTimeField(null=True, blank=True)
next_scan_at = models.DateTimeField(null=True, blank=True)
scheduler_task = models.ForeignKey(
PeriodicTask, on_delete=models.CASCADE, null=True, blank=True
PeriodicTask, on_delete=models.SET_NULL, null=True, blank=True
)
output_location = models.CharField(blank=True, null=True, max_length=4096)
provider = models.ForeignKey(
Provider,
on_delete=models.CASCADE,
related_name="scans",
related_query_name="scan",
)
task = models.ForeignKey(
Task,
on_delete=models.CASCADE,
related_name="scans",
related_query_name="scan",
null=True,
blank=True,
)
processor = models.ForeignKey(
"Processor",
on_delete=models.SET_NULL,
related_name="scans",
related_query_name="scan",
null=True,
blank=True,
)
output_location = models.CharField(blank=True, null=True, max_length=200)
# TODO: mutelist foreign key
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "scans"
@@ -468,6 +564,13 @@ class Scan(RowLevelSecurityProtectedModel):
condition=Q(state=StateChoices.COMPLETED),
name="scans_prov_state_ins_desc_idx",
),
# TODO This might replace `scans_prov_state_ins_desc_idx` completely. Review usage
models.Index(
fields=["tenant_id", "provider_id", "-inserted_at"],
condition=Q(state=StateChoices.COMPLETED),
include=["id"],
name="scans_prov_ins_desc_idx",
),
]
class JSONAPIMeta:
@@ -553,6 +656,8 @@ class Resource(RowLevelSecurityProtectedModel):
details = models.TextField(blank=True, null=True)
partition = models.TextField(blank=True, null=True)
failed_findings_count = models.IntegerField(default=0)
# Relationships
tags = models.ManyToManyField(
ResourceTag,
@@ -599,6 +704,10 @@ class Resource(RowLevelSecurityProtectedModel):
fields=["tenant_id", "provider_id"],
name="resources_tenant_provider_idx",
),
models.Index(
fields=["tenant_id", "-failed_findings_count", "id"],
name="resources_failed_findings_idx",
),
]
constraints = [
@@ -697,6 +806,9 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
check_id = models.CharField(max_length=100, blank=False, null=False)
check_metadata = models.JSONField(default=dict, null=False)
muted = models.BooleanField(default=False, null=False)
muted_reason = models.TextField(
blank=True, null=True, validators=[MinLengthValidator(3)], max_length=500
)
compliance = models.JSONField(default=dict, null=True, blank=True)
# Denormalize resource data for performance
@@ -838,6 +950,16 @@ class ResourceFindingMapping(PostgresPartitionedModel, RowLevelSecurityProtected
# - tenant_id
# - id
indexes = [
models.Index(
fields=["tenant_id", "finding_id"],
name="rfm_tenant_finding_idx",
),
models.Index(
fields=["tenant_id", "resource_id"],
name="rfm_tenant_resource_idx",
),
]
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "resource_id", "finding_id"),
@@ -942,6 +1064,11 @@ class Invitation(RowLevelSecurityProtectedModel):
null=True,
)
def save(self, *args, **kwargs):
if self.email:
self.email = self.email.strip().lower()
super().save(*args, **kwargs)
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "invitations"
@@ -1171,6 +1298,8 @@ class ComplianceRequirementOverview(RowLevelSecurityProtectedModel):
passed_checks = models.IntegerField(default=0)
failed_checks = models.IntegerField(default=0)
total_checks = models.IntegerField(default=0)
passed_findings = models.IntegerField(default=0)
total_findings = models.IntegerField(default=0)
scan = models.ForeignKey(
Scan,
@@ -1296,7 +1425,7 @@ class ScanSummary(RowLevelSecurityProtectedModel):
class Integration(RowLevelSecurityProtectedModel):
class IntegrationChoices(models.TextChoices):
S3 = "amazon_s3", _("Amazon S3")
AMAZON_S3 = "amazon_s3", _("Amazon S3")
AWS_SECURITY_HUB = "aws_security_hub", _("AWS Security Hub")
JIRA = "jira", _("JIRA")
SLACK = "slack", _("Slack")
@@ -1370,6 +1499,26 @@ class IntegrationProviderRelationship(RowLevelSecurityProtectedModel):
]
class SAMLToken(models.Model):
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)
expires_at = models.DateTimeField(editable=False)
token = models.JSONField(unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
db_table = "saml_tokens"
def save(self, *args, **kwargs):
if not self.expires_at:
self.expires_at = datetime.now(timezone.utc) + timedelta(seconds=15)
super().save(*args, **kwargs)
def is_expired(self) -> bool:
return datetime.now(timezone.utc) >= self.expires_at
class SAMLDomainIndex(models.Model):
"""
Public index of SAML domains. No RLS. Used for fast lookup in SAML login flow.
@@ -1447,7 +1596,7 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
),
]
def clean(self, old_email_domain=None):
def clean(self, old_email_domain=None, is_create=False):
# Domain must not contain @
if "@" in self.email_domain:
raise ValidationError({"email_domain": "Domain must not contain @"})
@@ -1471,6 +1620,25 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
{"tenant": "There is a problem with your email domain."}
)
# The entityID must be unique in the system
idp_settings = self._parsed_metadata
entity_id = idp_settings.get("entity_id")
if entity_id:
# Find any SocialApp with this entityID
q = SocialApp.objects.filter(provider="saml", provider_id=entity_id)
# If updating, exclude our own SocialApp from the check
if not is_create:
q = q.exclude(client_id=old_email_domain)
else:
q = q.exclude(client_id=self.email_domain)
if q.exists():
raise ValidationError(
{"metadata_xml": "There is a problem with your metadata."}
)
def save(self, *args, **kwargs):
self.email_domain = self.email_domain.strip().lower()
is_create = not SAMLConfiguration.objects.filter(pk=self.pk).exists()
@@ -1483,7 +1651,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
old_email_domain = None
old_metadata_xml = None
self.clean(old_email_domain)
self._parsed_metadata = self._parse_metadata()
self.clean(old_email_domain, is_create)
super().save(*args, **kwargs)
if is_create or (
@@ -1501,6 +1670,12 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
email_domain=self.email_domain, defaults={"tenant": self.tenant}
)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
SocialApp.objects.filter(provider="saml", client_id=self.email_domain).delete()
SAMLDomainIndex.objects.filter(email_domain=self.email_domain).delete()
def _parse_metadata(self):
"""
Parse the raw IdP metadata XML and extract:
@@ -1520,6 +1695,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
# Entity ID
entity_id = root.attrib.get("entityID")
if not entity_id:
raise ValidationError({"metadata_xml": "Missing entityID in metadata."})
# SSO endpoint (must exist)
sso = root.find(".//md:IDPSSODescriptor/md:SingleSignOnService", ns)
@@ -1558,9 +1735,8 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
Create or update the corresponding SocialApp based on email_domain.
If the domain changed, update the matching SocialApp.
"""
idp_settings = self._parse_metadata()
settings_dict = SOCIALACCOUNT_PROVIDERS["saml"].copy()
settings_dict["idp"] = idp_settings
settings_dict["idp"] = self._parsed_metadata
current_site = Site.objects.get(id=settings.SITE_ID)
@@ -1568,19 +1744,24 @@ class SAMLConfiguration(RowLevelSecurityProtectedModel):
provider="saml", client_id=previous_email_domain or self.email_domain
)
client_id = self.email_domain[:191]
name = f"SAML-{self.email_domain}"[:40]
if social_app_qs.exists():
social_app = social_app_qs.first()
social_app.client_id = self.email_domain
social_app.name = f"{self.tenant.name} SAML ({self.email_domain})"
social_app.client_id = client_id
social_app.name = name
social_app.settings = settings_dict
social_app.provider_id = self._parsed_metadata["entity_id"]
social_app.save()
social_app.sites.set([current_site])
else:
social_app = SocialApp.objects.create(
provider="saml",
client_id=self.email_domain,
name=f"{self.tenant.name} SAML ({self.email_domain})",
client_id=client_id,
name=name,
settings=settings_dict,
provider_id=self._parsed_metadata["entity_id"],
)
social_app.sites.set([current_site])
@@ -1646,6 +1827,10 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel):
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")
GPT_5_2025_08_07 = "gpt-5-2025-08-07", _("GPT-5 v2025-08-07")
GPT_5 = "gpt-5", _("GPT-5 Default")
GPT_5_MINI_2025_08_07 = "gpt-5-mini-2025-08-07", _("GPT-5 Mini v2025-08-07")
GPT_5_MINI = "gpt-5-mini", _("GPT-5 Mini Default")
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
@@ -1759,3 +1944,42 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel):
class JSONAPIMeta:
resource_name = "lighthouse-configurations"
class Processor(RowLevelSecurityProtectedModel):
class ProcessorChoices(models.TextChoices):
MUTELIST = "mutelist", _("Mutelist")
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)
processor_type = ProcessorTypeEnumField(choices=ProcessorChoices.choices)
configuration = models.JSONField(default=dict)
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "processors"
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "processor_type"),
name="unique_processor_types_tenant",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
indexes = [
models.Index(
fields=["tenant_id", "id"],
name="processor_tenant_id_idx",
),
models.Index(
fields=["tenant_id", "processor_type"],
name="processor_tenant_type_idx",
),
]
class JSONAPIMeta:
resource_name = "processors"
+2 -1
View File
@@ -11,11 +11,12 @@ class APIJSONRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
request = renderer_context.get("request")
tenant_id = getattr(request, "tenant_id", None) if request else None
db_alias = getattr(request, "db_alias", None) if request else None
include_param_present = "include" in request.query_params if request else False
# Use rls_transaction if needed for included resources, otherwise do nothing
context_manager = (
rls_transaction(tenant_id)
rls_transaction(tenant_id, using=db_alias)
if tenant_id and include_param_present
else nullcontext()
)
+16
View File
@@ -0,0 +1,16 @@
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from drf_spectacular.openapi import AutoSchema
class CombinedJWTOrAPIKeyAuthenticationScheme(OpenApiAuthenticationExtension):
target_class = "api.authentication.CombinedJWTOrAPIKeyAuthentication"
name = "JWT or API Key"
def get_security_definition(self, auto_schema: AutoSchema): # noqa: F841
return {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Supports both JWT Bearer tokens and API Key authentication. "
"Use `Bearer <token>` for JWT or `Api-Key <key>` for API keys.",
}
+95
View File
@@ -0,0 +1,95 @@
def _pick_task_response_component(components):
schemas = components.get("schemas", {}) or {}
for candidate in ("TaskResponse",):
if candidate in schemas:
return candidate
return None
def _extract_task_example_from_components(components):
schemas = components.get("schemas", {}) or {}
candidate = "TaskResponse"
doc = schemas.get(candidate)
if isinstance(doc, dict) and "example" in doc:
return doc["example"]
res = schemas.get(candidate)
if isinstance(res, dict) and "example" in res:
example = res["example"]
return example if "data" in example else {"data": example}
# Fallback
return {
"data": {
"type": "tasks",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"attributes": {
"inserted_at": "2019-08-24T14:15:22Z",
"completed_at": "2019-08-24T14:15:22Z",
"name": "string",
"state": "available",
"result": None,
"task_args": None,
"metadata": None,
},
}
}
def attach_task_202_examples(result, generator, request, public): # noqa: F841
if not isinstance(result, dict):
return result
components = result.get("components", {}) or {}
task_resp_component = _pick_task_response_component(components)
task_example = _extract_task_example_from_components(components)
paths = result.get("paths", {}) or {}
for path_item in paths.values():
if not isinstance(path_item, dict):
continue
for method_obj in path_item.values():
if not isinstance(method_obj, dict):
continue
responses = method_obj.get("responses", {}) or {}
resp_202 = responses.get("202")
if not isinstance(resp_202, dict):
continue
content = resp_202.get("content", {}) or {}
jsonapi = content.get("application/vnd.api+json")
if not isinstance(jsonapi, dict):
continue
# Inject example if missing
if "examples" not in jsonapi and "example" not in jsonapi:
jsonapi["examples"] = {
"Task queued": {
"summary": "Task queued",
"value": task_example,
}
}
# Rewrite schema $ref if needed
if task_resp_component:
schema = jsonapi.get("schema")
must_replace = False
if not isinstance(schema, dict):
must_replace = True
else:
ref = schema.get("$ref")
if not ref:
must_replace = True
else:
current = ref.split("/")[-1]
if current != task_resp_component:
must_replace = True
if must_replace:
jsonapi["schema"] = {
"$ref": f"#/components/schemas/{task_resp_component}"
}
return result
+26 -2
View File
@@ -1,12 +1,12 @@
from celery import states
from celery.signals import before_task_publish
from config.celery import celery_app
from django.db.models.signals import post_delete
from django.db.models.signals import post_delete, pre_delete
from django.dispatch import receiver
from django_celery_results.backends.database import DatabaseBackend
from api.db_utils import delete_related_daily_task
from api.models import Provider
from api.models import Membership, Provider, TenantAPIKey, User
def create_task_result_on_publish(sender=None, headers=None, **kwargs): # noqa: F841
@@ -32,3 +32,27 @@ before_task_publish.connect(
def delete_provider_scan_task(sender, instance, **kwargs): # noqa: F841
# Delete the associated periodic task when the provider is deleted
delete_related_daily_task(instance.id)
@receiver(pre_delete, sender=User)
def revoke_user_api_keys(sender, instance, **kwargs): # noqa: F841
"""
Revoke all API keys associated with a user before deletion.
The entity field will be set to NULL by on_delete=SET_NULL,
but we explicitly revoke the keys to prevent further use.
"""
TenantAPIKey.objects.filter(entity=instance).update(revoked=True)
@receiver(post_delete, sender=Membership)
def revoke_membership_api_keys(sender, instance, **kwargs): # noqa: F841
"""
Revoke all API keys when a user is removed from a tenant.
When a membership is deleted, all API keys created by that user
in that tenant should be revoked to prevent further access.
"""
TenantAPIKey.objects.filter(
entity=instance.user, tenant_id=instance.tenant.id
).update(revoked=True)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,7 @@ def test_delete_provider_without_executing_task(
client = APIClient()
test_user = "test_email@prowler.com"
test_password = "test_password"
test_password = "Test_password1@"
prowler_task = tasks_fixture[0]
task_mock = Mock()
+21 -26
View File
@@ -1,12 +1,10 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from allauth.socialaccount.models import SocialLogin
from django.contrib.auth import get_user_model
from api.adapters import ProwlerSocialAccountAdapter
from api.db_router import MainRouter
from api.models import Membership, SAMLConfiguration, Tenant
User = get_user_model()
@@ -27,7 +25,8 @@ class TestProwlerSocialAccountAdapter:
sociallogin = MagicMock(spec=SocialLogin)
sociallogin.account = MagicMock()
sociallogin.account.provider = "saml"
sociallogin.provider = MagicMock()
sociallogin.provider.id = "saml"
sociallogin.account.extra_data = {}
sociallogin.user = create_test_user
sociallogin.connect = MagicMock()
@@ -46,7 +45,9 @@ class TestProwlerSocialAccountAdapter:
sociallogin = MagicMock(spec=SocialLogin)
sociallogin.account = MagicMock()
sociallogin.account.provider = "github"
sociallogin.provider = MagicMock()
sociallogin.user = MagicMock()
sociallogin.provider.id = "saml"
sociallogin.account.extra_data = {}
sociallogin.connect = MagicMock()
@@ -54,29 +55,23 @@ class TestProwlerSocialAccountAdapter:
sociallogin.connect.assert_not_called()
def test_save_user_saml_flow(
self,
rf,
saml_setup,
saml_sociallogin,
):
def test_save_user_saml_sets_session_flag(self, rf):
adapter = ProwlerSocialAccountAdapter()
request = rf.get("/")
saml_sociallogin.user.email = saml_setup["email"]
request.session = {}
tenant = Tenant.objects.using(MainRouter.admin_db).get(
id=saml_setup["tenant_id"]
)
saml_config = SAMLConfiguration.objects.using(MainRouter.admin_db).get(
tenant=tenant
)
assert saml_config.email_domain == saml_setup["domain"]
sociallogin = MagicMock(spec=SocialLogin)
sociallogin.provider = MagicMock()
sociallogin.provider.id = "saml"
sociallogin.account = MagicMock()
sociallogin.account.extra_data = {}
user = adapter.save_user(request, saml_sociallogin)
mock_user = MagicMock()
mock_user.id = 123
assert user.email == saml_setup["email"]
assert (
Membership.objects.using(MainRouter.admin_db)
.filter(user=user, tenant=tenant)
.exists()
)
with patch("api.adapters.super") as mock_super:
with patch("api.adapters.transaction"):
with patch("api.adapters.MainRouter"):
mock_super.return_value.save_user.return_value = mock_user
adapter.save_user(request, sociallogin)
assert request.session["saml_user_created"] == "123"
+152
View File
@@ -0,0 +1,152 @@
import os
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from django.conf import settings
import api.apps as api_apps_module
from api.apps import (
ApiConfig,
PRIVATE_KEY_FILE,
PUBLIC_KEY_FILE,
SIGNING_KEY_ENV,
VERIFYING_KEY_ENV,
)
@pytest.fixture(autouse=True)
def reset_keys_initialized(monkeypatch):
"""Ensure per-test clean state for the module-level guard flag."""
monkeypatch.setattr(api_apps_module, "_keys_initialized", False, raising=False)
def _stub_keys():
return (
"""-----BEGIN PRIVATE KEY-----\nPRIVATE\n-----END PRIVATE KEY-----\n""",
"""-----BEGIN PUBLIC KEY-----\nPUBLIC\n-----END PUBLIC KEY-----\n""",
)
def test_generate_jwt_keys_when_missing(monkeypatch, tmp_path):
# Arrange: isolate FS, env, and settings; force generation path
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.delenv(SIGNING_KEY_ENV, raising=False)
monkeypatch.delenv(VERIFYING_KEY_ENV, raising=False)
# Work on a copy of SIMPLE_JWT to avoid mutating the global settings dict for other tests
monkeypatch.setattr(
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
# Avoid dependency on the cryptography package
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_stub_keys))
config = ApiConfig("api", api_apps_module)
# Act
config._ensure_crypto_keys()
# Assert: files created with expected content
priv_path = Path(tmp_path) / PRIVATE_KEY_FILE
pub_path = Path(tmp_path) / PUBLIC_KEY_FILE
assert priv_path.is_file()
assert pub_path.is_file()
assert priv_path.read_text() == _stub_keys()[0]
assert pub_path.read_text() == _stub_keys()[1]
# Env vars and Django settings updated
assert os.environ[SIGNING_KEY_ENV] == _stub_keys()[0]
assert os.environ[VERIFYING_KEY_ENV] == _stub_keys()[1]
assert settings.SIMPLE_JWT["SIGNING_KEY"] == _stub_keys()[0]
assert settings.SIMPLE_JWT["VERIFYING_KEY"] == _stub_keys()[1]
def test_ensure_crypto_keys_are_idempotent_within_process(monkeypatch, tmp_path):
# Arrange
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.delenv(SIGNING_KEY_ENV, raising=False)
monkeypatch.delenv(VERIFYING_KEY_ENV, raising=False)
monkeypatch.setattr(
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
mock_generate = MagicMock(side_effect=_stub_keys)
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(mock_generate))
config = ApiConfig("api", api_apps_module)
# Act: first call should generate, second should be a no-op (guard flag)
config._ensure_crypto_keys()
config._ensure_crypto_keys()
# Assert: generation occurred exactly once
assert mock_generate.call_count == 1
def test_ensure_jwt_keys_uses_existing_files(monkeypatch, tmp_path):
# Arrange: pre-create key files
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.setattr(
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
)
existing_private, existing_public = _stub_keys()
(Path(tmp_path) / PRIVATE_KEY_FILE).write_text(existing_private)
(Path(tmp_path) / PUBLIC_KEY_FILE).write_text(existing_public)
# If generation were called, fail the test
def _fail_generate():
raise AssertionError("_generate_jwt_keys should not be called when files exist")
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_fail_generate))
config = ApiConfig("api", api_apps_module)
# Act: call the lower-level method directly to set env/settings from files
config._ensure_jwt_keys()
# Assert
# _read_key_file() strips trailing newlines; environment/settings should reflect stripped content
assert os.environ[SIGNING_KEY_ENV] == existing_private.strip()
assert os.environ[VERIFYING_KEY_ENV] == existing_public.strip()
assert settings.SIMPLE_JWT["SIGNING_KEY"] == existing_private.strip()
assert settings.SIMPLE_JWT["VERIFYING_KEY"] == existing_public.strip()
def test_ensure_crypto_keys_skips_when_env_vars(monkeypatch, tmp_path):
# Arrange: put values in env so the orchestrator doesn't generate
monkeypatch.setattr(
api_apps_module, "KEYS_DIRECTORY", Path(tmp_path), raising=False
)
monkeypatch.setenv(SIGNING_KEY_ENV, "ENV-PRIVATE")
monkeypatch.setenv(VERIFYING_KEY_ENV, "ENV-PUBLIC")
monkeypatch.setattr(
settings, "SIMPLE_JWT", settings.SIMPLE_JWT.copy(), raising=False
)
monkeypatch.setattr(settings, "TESTING", False, raising=False)
called = {"ensure": False}
def _track_call():
called["ensure"] = True
return _stub_keys()
monkeypatch.setattr(ApiConfig, "_generate_jwt_keys", staticmethod(_track_call))
config = ApiConfig("api", api_apps_module)
# Act
config._ensure_crypto_keys()
# Assert: orchestrator did not trigger generation when env present
assert called["ensure"] is False
@@ -0,0 +1,384 @@
import time
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
from uuid import uuid4
import pytest
from django.test import RequestFactory
from rest_framework.exceptions import AuthenticationFailed
from api.authentication import TenantAPIKeyAuthentication
from api.db_router import MainRouter
from api.models import TenantAPIKey
@pytest.mark.django_db
class TestTenantAPIKeyAuthentication:
@pytest.fixture
def auth_backend(self):
"""Create an instance of TenantAPIKeyAuthentication."""
return TenantAPIKeyAuthentication()
@pytest.fixture
def request_factory(self):
"""Create a Django request factory."""
return RequestFactory()
def test_authenticate_credentials_uses_admin_database(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that _authenticate_credentials routes queries to admin database."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
# Extract the encrypted key part (after the prefix and separator)
_, encrypted_key = raw_key.split(TenantAPIKey.objects.separator, 1)
# Create a mock request
request = request_factory.get("/")
# Call the method
entity, auth_dict = auth_backend._authenticate_credentials(
request, encrypted_key
)
# Verify that the entity is the user associated with the API key
assert entity == api_key.entity
assert entity.id == api_key.entity.id
def test_authenticate_credentials_restores_manager_on_success(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that the manager is restored after successful authentication."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
_, encrypted_key = raw_key.split(TenantAPIKey.objects.separator, 1)
# Store the original manager
original_manager = TenantAPIKey.objects
request = request_factory.get("/")
# Call the method
auth_backend._authenticate_credentials(request, encrypted_key)
# Verify the manager was restored
assert TenantAPIKey.objects == original_manager
def test_authenticate_credentials_restores_manager_on_exception(
self, auth_backend, request_factory
):
"""Test that the manager is restored even when an exception occurs."""
# Store the original manager
original_manager = TenantAPIKey.objects
request = request_factory.get("/")
# Try to authenticate with an invalid key that will raise an exception
with pytest.raises(Exception):
auth_backend._authenticate_credentials(request, "invalid_encrypted_key")
# Verify the manager was restored despite the exception
assert TenantAPIKey.objects == original_manager
def test_authenticate_valid_api_key(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test successful authentication with a valid API key."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
# Create a request with the API key in the Authorization header
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# Authenticate
entity, auth_dict = auth_backend.authenticate(request)
# Verify the entity and auth dict
assert entity == api_key.entity
assert auth_dict["tenant_id"] == str(api_key.tenant_id)
assert auth_dict["sub"] == str(api_key.entity.id)
assert auth_dict["api_key_prefix"] == api_key.prefix
# Verify that last_used_at was updated
api_key.refresh_from_db()
assert api_key.last_used_at is not None
assert (datetime.now(timezone.utc) - api_key.last_used_at).seconds < 5
def test_authenticate_valid_api_key_uses_admin_database(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that authenticate uses admin database for API key lookup."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# Mock the manager's using method to verify it's called with admin_db
with patch.object(
TenantAPIKey.objects, "using", wraps=TenantAPIKey.objects.using
) as mock_using:
auth_backend.authenticate(request)
# Verify that .using('admin') was called
mock_using.assert_called_with(MainRouter.admin_db)
def test_authenticate_invalid_key_format_missing_separator(
self, auth_backend, request_factory
):
"""Test authentication fails with invalid API key format (no separator)."""
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = "Api-Key invalid_key_no_separator"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "Invalid API Key."
def test_authenticate_invalid_key_format_empty_prefix(
self, auth_backend, request_factory
):
"""Test authentication fails with empty prefix."""
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = "Api-Key .encrypted_part"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "Invalid API Key."
def test_authenticate_invalid_encrypted_key(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test authentication fails with invalid encrypted key."""
api_key = api_keys_fixture[0]
# Create an invalid key with valid prefix but invalid encryption
invalid_key = f"{api_key.prefix}.invalid_encrypted_data"
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {invalid_key}"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "Invalid API Key."
def test_authenticate_revoked_api_key(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test authentication fails with a revoked API key."""
# Use the revoked API key (index 2 from fixture)
api_key = api_keys_fixture[2]
raw_key = api_key._raw_key
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# The revoked key should fail during credential validation
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "This API Key has been revoked."
def test_authenticate_expired_api_key(
self, auth_backend, create_test_user, tenants_fixture, request_factory
):
"""Test authentication fails with an expired API key."""
tenant = tenants_fixture[0]
user = create_test_user
# Create an expired API key
api_key, raw_key = TenantAPIKey.objects.create_api_key(
name="Expired API Key",
tenant_id=tenant.id,
entity=user,
expiry_date=datetime.now(timezone.utc) - timedelta(days=1),
)
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "API Key has already expired."
def test_authenticate_nonexistent_api_key(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test authentication fails when API key doesn't exist in database."""
# Create a valid-looking encrypted key with a non-existent UUID
api_key = api_keys_fixture[0]
non_existent_uuid = str(uuid4())
# Manually create an encrypted key with a non-existent ID
payload = {
"_pk": non_existent_uuid,
"_exp": (datetime.now(timezone.utc) + timedelta(days=30)).timestamp(),
}
encrypted_key = auth_backend.key_crypto.generate(payload)
fake_key = f"{api_key.prefix}.{encrypted_key}"
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {fake_key}"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "No entity matching this api key."
def test_authenticate_updates_last_used_at(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that last_used_at is updated on successful authentication."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
# Store the original last_used_at
original_last_used = api_key.last_used_at
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# Authenticate
auth_backend.authenticate(request)
# Refresh from database
api_key.refresh_from_db()
# Verify last_used_at was updated
assert api_key.last_used_at is not None
if original_last_used:
assert api_key.last_used_at > original_last_used
def test_authenticate_saves_to_admin_database(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that the API key save operation uses admin database."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# Mock the save method to verify it's called with using='admin'
with patch.object(TenantAPIKey, "save") as mock_save:
auth_backend.authenticate(request)
# Verify save was called with using=admin_db
mock_save.assert_called_once_with(
update_fields=["last_used_at"], using=MainRouter.admin_db
)
def test_authenticate_returns_correct_auth_dict(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that the auth dict contains all required fields."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
entity, auth_dict = auth_backend.authenticate(request)
# Verify all required fields are present
assert "tenant_id" in auth_dict
assert "sub" in auth_dict
assert "api_key_prefix" in auth_dict
# Verify values are correct
assert auth_dict["tenant_id"] == str(api_key.tenant_id)
assert auth_dict["sub"] == str(api_key.entity.id)
assert auth_dict["api_key_prefix"] == api_key.prefix
def test_authenticate_with_multiple_api_keys_same_tenant(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test that authentication works correctly with multiple API keys for the same tenant."""
# Test with first API key
api_key1 = api_keys_fixture[0]
raw_key1 = api_key1._raw_key
request1 = request_factory.get("/")
request1.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key1}"
entity1, auth_dict1 = auth_backend.authenticate(request1)
assert entity1 == api_key1.entity
assert auth_dict1["api_key_prefix"] == api_key1.prefix
# Test with second API key
api_key2 = api_keys_fixture[1]
raw_key2 = api_key2._raw_key
request2 = request_factory.get("/")
request2.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key2}"
entity2, auth_dict2 = auth_backend.authenticate(request2)
assert entity2 == api_key2.entity
assert auth_dict2["api_key_prefix"] == api_key2.prefix
# Verify they're different keys but same tenant
assert auth_dict1["api_key_prefix"] != auth_dict2["api_key_prefix"]
assert auth_dict1["tenant_id"] == auth_dict2["tenant_id"]
def test_authenticate_with_wrong_prefix_in_db(
self, auth_backend, api_keys_fixture, request_factory
):
"""Test authentication fails when prefix doesn't match database."""
api_key = api_keys_fixture[0]
raw_key = api_key._raw_key
# Extract the encrypted part and combine with wrong prefix
_, encrypted_part = raw_key.split(TenantAPIKey.objects.separator, 1)
wrong_key = f"pk_wrong123.{encrypted_part}"
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {wrong_key}"
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "Invalid API Key."
def test_authenticate_credentials_exception_handling(
self, auth_backend, request_factory
):
"""Test that exceptions in _authenticate_credentials are properly handled."""
request = request_factory.get("/")
# Test with completely invalid data that will cause InvalidToken
with pytest.raises(Exception):
auth_backend._authenticate_credentials(request, "completely_invalid")
def test_authenticate_with_expired_timestamp(
self, auth_backend, create_test_user, tenants_fixture, request_factory
):
"""Test that expired timestamp in encrypted key causes authentication failure."""
tenant = tenants_fixture[0]
user = create_test_user
# Create an API key with a very short expiry
api_key, raw_key = TenantAPIKey.objects.create_api_key(
name="Short-lived API Key",
tenant_id=tenant.id,
entity=user,
expiry_date=datetime.now(timezone.utc) + timedelta(seconds=1),
)
# Wait for the key to expire
time.sleep(2)
request = request_factory.get("/")
request.META["HTTP_AUTHORIZATION"] = f"Api-Key {raw_key}"
# Should fail with expired key
with pytest.raises(AuthenticationFailed) as exc_info:
auth_backend.authenticate(request)
assert str(exc_info.value.detail) == "API Key has already expired."
@@ -239,6 +239,7 @@ class TestCompliance:
Framework="Framework 1",
Version="1.0",
Description="Description of compliance1",
Name="Compliance 1",
)
prowler_compliance = {"aws": {"compliance1": compliance1}}
@@ -248,6 +249,7 @@ class TestCompliance:
"aws": {
"compliance1": {
"framework": "Framework 1",
"name": "Compliance 1",
"version": "1.0",
"provider": "aws",
"description": "Description of compliance1",
+112
View File
@@ -11,8 +11,10 @@ from api.db_utils import (
batch_delete,
create_objects_in_batches,
enum_to_choices,
generate_api_key_prefix,
generate_random_token,
one_week_from_now,
update_objects_in_batches,
)
from api.models import Provider
@@ -227,3 +229,113 @@ class TestCreateObjectsInBatches:
qs = Provider.objects.filter(tenant=tenant)
assert qs.count() == total
@pytest.mark.django_db
class TestUpdateObjectsInBatches:
@pytest.fixture
def tenant(self, tenants_fixture):
return tenants_fixture[0]
def make_provider_instances(self, tenant, count):
"""
Return a list of `count` unsaved Provider instances for the given tenant.
"""
base_uid = 2000
return [
Provider(
tenant=tenant,
uid=str(base_uid + i),
provider=Provider.ProviderChoices.AWS,
)
for i in range(count)
]
def test_exact_multiple_of_batch(self, tenant):
total = 6
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
# Fetch them back, mutate the `uid` field, then update in batches
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_non_multiple_of_batch(self, tenant):
total = 7
batch_size = 3
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs, batch_size=batch_size)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
batch_size=batch_size,
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
def test_batch_size_default(self, tenant):
default_size = settings.DJANGO_DELETION_BATCH_SIZE
total = default_size + 2
objs = self.make_provider_instances(tenant, total)
create_objects_in_batches(str(tenant.id), Provider, objs)
providers = list(Provider.objects.filter(tenant=tenant))
for p in providers:
p.uid = f"{p.uid}_upd"
# Update without specifying batch_size (uses default)
update_objects_in_batches(
tenant_id=str(tenant.id),
model=Provider,
objects=providers,
fields=["uid"],
)
qs = Provider.objects.filter(tenant=tenant, uid__endswith="_upd")
assert qs.count() == total
class TestGenerateApiKeyPrefix:
def test_prefix_format(self):
"""Test that generated prefix starts with 'pk_'."""
prefix = generate_api_key_prefix()
assert prefix.startswith("pk_")
def test_prefix_length(self):
"""Test that prefix has correct length (pk_ + 8 random chars = 11)."""
prefix = generate_api_key_prefix()
assert len(prefix) == 11
def test_prefix_uniqueness(self):
"""Test that multiple generations produce unique prefixes."""
prefixes = {generate_api_key_prefix() for _ in range(100)}
assert len(prefixes) == 100
def test_prefix_character_set(self):
"""Test that random part uses only allowed characters."""
allowed_chars = "23456789ABCDEFGHJKMNPQRSTVWXYZ"
for _ in range(50):
prefix = generate_api_key_prefix()
random_part = prefix[3:] # Strip 'pk_'
assert all(char in allowed_chars for char in random_part)
@@ -24,6 +24,7 @@ def test_api_logging_middleware_logging(mock_logger):
mock_extract_auth_info.return_value = {
"user_id": "user123",
"tenant_id": "tenant456",
"api_key_prefix": "pk_test",
}
with patch("api.middleware.logging.getLogger") as mock_get_logger:
@@ -44,6 +45,7 @@ def test_api_logging_middleware_logging(mock_logger):
expected_extra = {
"user_id": "user123",
"tenant_id": "tenant456",
"api_key_prefix": "pk_test",
"method": "GET",
"path": "/test-path",
"query_params": {"param1": "value1", "param2": "value2"},
+71 -16
View File
@@ -3,7 +3,7 @@ from allauth.socialaccount.models import SocialApp
from django.core.exceptions import ValidationError
from api.db_router import MainRouter
from api.models import Resource, ResourceTag, SAMLConfiguration, Tenant
from api.models import Resource, ResourceTag, SAMLConfiguration, SAMLDomainIndex
@pytest.mark.django_db
@@ -142,8 +142,8 @@ class TestSAMLConfigurationModel:
</md:EntityDescriptor>
"""
def test_creates_valid_configuration(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant A")
def test_creates_valid_configuration(self, tenants_fixture):
tenant = tenants_fixture[0]
config = SAMLConfiguration.objects.using(MainRouter.admin_db).create(
email_domain="ssoexample.com",
metadata_xml=TestSAMLConfigurationModel.VALID_METADATA,
@@ -153,8 +153,8 @@ class TestSAMLConfigurationModel:
assert config.email_domain == "ssoexample.com"
assert SocialApp.objects.filter(client_id="ssoexample.com").exists()
def test_email_domain_with_at_symbol_fails(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant B")
def test_email_domain_with_at_symbol_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
config = SAMLConfiguration(
email_domain="invalid@domain.com",
metadata_xml=TestSAMLConfigurationModel.VALID_METADATA,
@@ -168,9 +168,8 @@ class TestSAMLConfigurationModel:
assert "email_domain" in errors
assert "Domain must not contain @" in errors["email_domain"][0]
def test_duplicate_email_domain_fails(self):
tenant1 = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant C1")
tenant2 = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant C2")
def test_duplicate_email_domain_fails(self, tenants_fixture):
tenant1, tenant2, *_ = tenants_fixture
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
email_domain="duplicate.com",
@@ -191,8 +190,8 @@ class TestSAMLConfigurationModel:
assert "tenant" in errors
assert "There is a problem with your email domain." in errors["tenant"][0]
def test_duplicate_tenant_config_fails(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant D")
def test_duplicate_tenant_config_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
email_domain="unique1.com",
@@ -216,8 +215,8 @@ class TestSAMLConfigurationModel:
in errors["tenant"][0]
)
def test_invalid_metadata_xml_fails(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant E")
def test_invalid_metadata_xml_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
config = SAMLConfiguration(
email_domain="brokenxml.com",
metadata_xml="<bad<xml>",
@@ -232,8 +231,8 @@ class TestSAMLConfigurationModel:
assert "Invalid XML" in errors["metadata_xml"][0]
assert "not well-formed" in errors["metadata_xml"][0]
def test_metadata_missing_sso_fails(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant F")
def test_metadata_missing_sso_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
xml = """<md:EntityDescriptor entityID="x" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
<md:IDPSSODescriptor></md:IDPSSODescriptor>
</md:EntityDescriptor>"""
@@ -250,8 +249,8 @@ class TestSAMLConfigurationModel:
assert "metadata_xml" in errors
assert "Missing SingleSignOnService" in errors["metadata_xml"][0]
def test_metadata_missing_certificate_fails(self):
tenant = Tenant.objects.using(MainRouter.admin_db).create(name="Tenant G")
def test_metadata_missing_certificate_fails(self, tenants_fixture):
tenant = tenants_fixture[0]
xml = """<md:EntityDescriptor entityID="x" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
<md:IDPSSODescriptor>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/sso"/>
@@ -269,3 +268,59 @@ class TestSAMLConfigurationModel:
errors = exc_info.value.message_dict
assert "metadata_xml" in errors
assert "X509Certificate" in errors["metadata_xml"][0]
def test_deletes_saml_configuration_and_related_objects(self, tenants_fixture):
tenant = tenants_fixture[0]
email_domain = "deleteme.com"
# Create the configuration
config = SAMLConfiguration.objects.using(MainRouter.admin_db).create(
email_domain=email_domain,
metadata_xml=TestSAMLConfigurationModel.VALID_METADATA,
tenant=tenant,
)
# Verify that the SocialApp and SAMLDomainIndex exist
assert SocialApp.objects.filter(client_id=email_domain).exists()
assert (
SAMLDomainIndex.objects.using(MainRouter.admin_db)
.filter(email_domain=email_domain)
.exists()
)
# Delete the configuration
config.delete()
# Verify that the configuration and its related objects are deleted
assert (
not SAMLConfiguration.objects.using(MainRouter.admin_db)
.filter(pk=config.pk)
.exists()
)
assert not SocialApp.objects.filter(client_id=email_domain).exists()
assert (
not SAMLDomainIndex.objects.using(MainRouter.admin_db)
.filter(email_domain=email_domain)
.exists()
)
def test_duplicate_entity_id_fails_on_creation(self, tenants_fixture):
tenant1, tenant2, *_ = tenants_fixture
SAMLConfiguration.objects.using(MainRouter.admin_db).create(
email_domain="first.com",
metadata_xml=self.VALID_METADATA,
tenant=tenant1,
)
config = SAMLConfiguration(
email_domain="second.com",
metadata_xml=self.VALID_METADATA,
tenant=tenant2,
)
with pytest.raises(ValidationError) as exc_info:
config.save()
errors = exc_info.value.message_dict
assert "metadata_xml" in errors
assert "There is a problem with your metadata." in errors["metadata_xml"][0]
+424 -3
View File
@@ -1,6 +1,8 @@
import json
from unittest.mock import ANY, Mock, patch
import pytest
from conftest import TODAY
from django.urls import reverse
from rest_framework import status
@@ -60,7 +62,7 @@ class TestUserViewSet:
def test_create_user_with_all_permissions(self, authenticated_client_rbac):
valid_user_payload = {
"name": "test",
"password": "newpassword123",
"password": "Newpassword123@",
"email": "new_user@test.com",
}
response = authenticated_client_rbac.post(
@@ -74,7 +76,7 @@ class TestUserViewSet:
):
valid_user_payload = {
"name": "test",
"password": "newpassword123",
"password": "Newpassword123@",
"email": "new_user@test.com",
}
response = authenticated_client_no_permissions_rbac.post(
@@ -150,6 +152,221 @@ class TestUserViewSet:
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["email"] == "rbac_limited@rbac.com"
def test_me_shows_own_roles_and_memberships_without_manage_account(
self, authenticated_client_no_permissions_rbac
):
response = authenticated_client_no_permissions_rbac.get(reverse("user-me"))
assert response.status_code == status.HTTP_200_OK
rels = response.json()["data"]["relationships"]
# Self should see own roles and memberships even without manage_account
assert isinstance(rels["roles"]["data"], list)
assert rels["memberships"]["meta"]["count"] == 1
def test_me_shows_roles_and_memberships_with_manage_account(
self, authenticated_client_rbac
):
response = authenticated_client_rbac.get(reverse("user-me"))
assert response.status_code == status.HTTP_200_OK
rels = response.json()["data"]["relationships"]
# Roles should have data when manage_account is True
assert len(rels["roles"]["data"]) > 0
# Memberships should be present and count > 0
assert rels["memberships"]["meta"]["count"] > 0
def test_me_include_roles_and_memberships_included_block(
self, authenticated_client_rbac
):
# Request current user info including roles and memberships
response = authenticated_client_rbac.get(
reverse("user-me"), {"include": "roles,memberships"}
)
assert response.status_code == status.HTTP_200_OK
payload = response.json()
# Included must contain memberships corresponding to relationships data
rel_memberships = payload["data"]["relationships"]["memberships"]
ids_in_relationship = {item["id"] for item in rel_memberships["data"]}
included = payload["included"]
included_membership_ids = {
item["id"] for item in included if item["type"] == "memberships"
}
# If there are memberships in relationships, they must be present in included
if ids_in_relationship:
assert ids_in_relationship.issubset(included_membership_ids)
else:
# At minimum, included should contain the user's membership when requested
# (count should align with meta count)
assert rel_memberships["meta"]["count"] == len(included_membership_ids)
def test_list_users_with_manage_account_only_forbidden(
self, authenticated_client_rbac_manage_account
):
response = authenticated_client_rbac_manage_account.get(reverse("user-list"))
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_retrieve_other_user_with_manage_account_only_forbidden(
self, authenticated_client_rbac_manage_account, create_test_user
):
response = authenticated_client_rbac_manage_account.get(
reverse("user-detail", kwargs={"pk": create_test_user.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_list_users_with_manage_users_only_hides_relationships(
self, authenticated_client_rbac_manage_users_only
):
# Ensure there is at least one other user in the same tenant
mu_user = authenticated_client_rbac_manage_users_only.user
mu_membership = Membership.objects.filter(user=mu_user).first()
tenant = mu_membership.tenant
other_user = User.objects.create_user(
name="other_in_tenant",
email="other_in_tenant@rbac.com",
password="Password123@",
)
Membership.objects.create(user=other_user, tenant=tenant)
response = authenticated_client_rbac_manage_users_only.get(reverse("user-list"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert isinstance(data, list)
current_user_id = str(mu_user.id)
assert any(item["id"] == current_user_id for item in data)
for item in data:
rels = item["relationships"]
if item["id"] == current_user_id:
# Self should see own relationships
assert isinstance(rels["roles"]["data"], list)
assert rels["memberships"]["meta"].get("count", 0) >= 1
else:
# Others should be hidden without manage_account
assert rels["roles"]["data"] == []
assert rels["memberships"]["data"] == []
assert rels["memberships"]["meta"]["count"] == 0
def test_include_roles_hidden_without_manage_account(
self, authenticated_client_rbac_manage_users_only
):
# Arrange: ensure another user in the same tenant with its own role
mu_user = authenticated_client_rbac_manage_users_only.user
mu_membership = Membership.objects.filter(user=mu_user).first()
tenant = mu_membership.tenant
other_user = User.objects.create_user(
name="other_in_tenant_inc",
email="other_in_tenant_inc@rbac.com",
password="Password123@",
)
Membership.objects.create(user=other_user, tenant=tenant)
other_role = Role.objects.create(
name="other_inc_role",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
)
UserRoleRelationship.objects.create(
user=other_user, role=other_role, tenant_id=tenant.id
)
response = authenticated_client_rbac_manage_users_only.get(
reverse("user-list"), {"include": "roles"}
)
assert response.status_code == status.HTTP_200_OK
payload = response.json()
# Assert: included must not contain the other user's role
included = payload.get("included", [])
included_role_ids = {
item["id"] for item in included if item.get("type") == "roles"
}
assert str(other_role.id) not in included_role_ids
# Relationships for other user should be empty
for item in payload["data"]:
if item["id"] == str(other_user.id):
rels = item["relationships"]
assert rels["roles"]["data"] == []
def test_include_roles_visible_with_manage_account(
self, authenticated_client_rbac, tenants_fixture
):
# Arrange: another user in tenant[0] with its role
tenant = tenants_fixture[0]
other_user = User.objects.create_user(
name="other_with_role",
email="other_with_role@rbac.com",
password="Password123@",
)
Membership.objects.create(user=other_user, tenant=tenant)
other_role = Role.objects.create(
name="other_visible_role",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
)
UserRoleRelationship.objects.create(
user=other_user, role=other_role, tenant_id=tenant.id
)
response = authenticated_client_rbac.get(
reverse("user-list"), {"include": "roles"}
)
assert response.status_code == status.HTTP_200_OK
payload = response.json()
# Assert: included must contain the other user's role
included = payload.get("included", [])
included_role_ids = {
item["id"] for item in included if item.get("type") == "roles"
}
assert str(other_role.id) in included_role_ids
def test_retrieve_user_with_manage_users_only_hides_relationships(
self, authenticated_client_rbac_manage_users_only
):
# Create a target user in the same tenant to ensure visibility
mu_user = authenticated_client_rbac_manage_users_only.user
mu_membership = Membership.objects.filter(user=mu_user).first()
tenant = mu_membership.tenant
target_user = User.objects.create_user(
name="target_same_tenant",
email="target_same_tenant@rbac.com",
password="Password123@",
)
Membership.objects.create(user=target_user, tenant=tenant)
response = authenticated_client_rbac_manage_users_only.get(
reverse("user-detail", kwargs={"pk": target_user.id})
)
assert response.status_code == status.HTTP_200_OK
rels = response.json()["data"]["relationships"]
assert rels["roles"]["data"] == []
assert rels["memberships"]["data"] == []
assert rels["memberships"]["meta"]["count"] == 0
def test_list_users_with_all_permissions_shows_relationships(
self, authenticated_client_rbac
):
response = authenticated_client_rbac.get(reverse("user-list"))
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert isinstance(data, list)
rels = data[0]["relationships"]
assert len(rels["roles"]["data"]) >= 0
assert rels["memberships"]["meta"]["count"] >= 0
@pytest.mark.django_db
class TestProviderViewSet:
@@ -321,7 +538,7 @@ class TestProviderViewSet:
@pytest.mark.django_db
class TestLimitedVisibility:
TEST_EMAIL = "rbac@rbac.com"
TEST_PASSWORD = "thisisapassword123"
TEST_PASSWORD = "Thisisapassword123@"
@pytest.fixture
def limited_admin_user(
@@ -409,3 +626,207 @@ class TestLimitedVisibility:
assert (
response.json()["data"]["relationships"]["providers"]["meta"]["count"] == 1
)
def test_overviews_providers(
self,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(reverse("overview-providers"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) > 0
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(reverse("overview-providers"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
@pytest.mark.parametrize(
"endpoint_name",
[
"findings",
"findings_severity",
],
)
def test_overviews_findings(
self,
endpoint_name,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(
reverse(f"overview-{endpoint_name}")
)
assert response.status_code == status.HTTP_200_OK
values = response.json()["data"]["attributes"].values()
assert any(value > 0 for value in values)
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(
reverse(f"overview-{endpoint_name}")
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]["attributes"].values()
assert all(value == 0 for value in data)
def test_overviews_services(
self,
authenticated_client_rbac_limited,
scan_summaries_fixture,
providers_fixture,
):
# By default, the associated provider is the one which has the overview data
response = authenticated_client_rbac_limited.get(
reverse("overview-services"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) > 0
# Changing the provider visibility, no data should be returned
# Only the associated provider to that group is changed
new_provider = providers_fixture[1]
ProviderGroupMembership.objects.all().update(provider=new_provider)
response = authenticated_client_rbac_limited.get(
reverse("overview-services"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
@pytest.mark.django_db
class TestRolePermissions:
def test_role_create_with_manage_account_only_allowed(
self, authenticated_client_rbac_manage_account
):
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Role Manage Account Only",
"manage_users": "false",
"manage_account": "true",
"manage_providers": "false",
"manage_scans": "false",
"unlimited_visibility": "false",
},
"relationships": {"provider_groups": {"data": []}},
}
}
response = authenticated_client_rbac_manage_account.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED
def test_role_create_with_manage_users_only_forbidden(
self, authenticated_client_rbac_manage_users_only
):
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Role Manage Users Only",
"manage_users": "true",
"manage_account": "false",
"manage_providers": "false",
"manage_scans": "false",
"unlimited_visibility": "false",
},
"relationships": {"provider_groups": {"data": []}},
}
}
response = authenticated_client_rbac_manage_users_only.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.django_db
class TestUserRoleLinkPermissions:
def test_link_user_roles_with_manage_account_only_allowed(
self, authenticated_client_rbac_manage_account
):
# Arrange: create a second user in the same tenant as the manage_account user
ma_user = authenticated_client_rbac_manage_account.user
ma_membership = Membership.objects.filter(user=ma_user).first()
tenant = ma_membership.tenant
user2 = User.objects.create_user(
name="target_user",
email="target_user_ma@rbac.com",
password="Password123@",
)
Membership.objects.create(user=user2, tenant=tenant)
# Create a role in the same tenant
role = Role.objects.create(
name="linkable_role",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
)
data = {"data": [{"type": "roles", "id": str(role.id)}]}
# Act
response = authenticated_client_rbac_manage_account.post(
reverse("user-roles-relationship", kwargs={"pk": user2.id}),
data=data,
content_type="application/vnd.api+json",
)
# Assert
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_link_user_roles_with_manage_users_only_forbidden(
self, authenticated_client_rbac_manage_users_only
):
mu_user = authenticated_client_rbac_manage_users_only.user
mu_membership = Membership.objects.filter(user=mu_user).first()
tenant = mu_membership.tenant
user2 = User.objects.create_user(
name="target_user2",
email="target_user_mu@rbac.com",
password="Password123@",
)
Membership.objects.create(user=user2, tenant=tenant)
role = Role.objects.create(
name="linkable_role_mu",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
)
data = {"data": [{"type": "roles", "id": str(role.id)}]}
response = authenticated_client_rbac_manage_users_only.post(
reverse("user-roles-relationship", kwargs={"pk": user2.id}),
data=data,
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
+80
View File
@@ -0,0 +1,80 @@
import logging
from unittest.mock import MagicMock
from config.settings.sentry import before_send
def test_before_send_ignores_log_with_ignored_exception():
"""Test that before_send ignores logs containing ignored exceptions."""
log_record = MagicMock()
log_record.msg = "Provider kubernetes is not connected"
log_record.levelno = logging.ERROR # 40
hint = {"log_record": log_record}
event = MagicMock()
result = before_send(event, hint)
# Assert that the event was dropped (None returned)
assert result is None
def test_before_send_ignores_exception_with_ignored_exception():
"""Test that before_send ignores exceptions containing ignored exceptions."""
exc_info = (Exception, Exception("Provider kubernetes is not connected"), None)
hint = {"exc_info": exc_info}
event = MagicMock()
result = before_send(event, hint)
# Assert that the event was dropped (None returned)
assert result is None
def test_before_send_passes_through_non_ignored_log():
"""Test that before_send passes through logs that don't contain ignored exceptions."""
log_record = MagicMock()
log_record.msg = "Some other error message"
log_record.levelno = logging.ERROR # 40
hint = {"log_record": log_record}
event = MagicMock()
result = before_send(event, hint)
# Assert that the event was passed through
assert result == event
def test_before_send_passes_through_non_ignored_exception():
"""Test that before_send passes through exceptions that don't contain ignored exceptions."""
exc_info = (Exception, Exception("Some other error message"), None)
hint = {"exc_info": exc_info}
event = MagicMock()
result = before_send(event, hint)
# Assert that the event was passed through
assert result == event
def test_before_send_handles_warning_level():
"""Test that before_send handles warning level logs."""
log_record = MagicMock()
log_record.msg = "Provider kubernetes is not connected"
log_record.levelno = logging.WARNING # 30
hint = {"log_record": log_record}
event = MagicMock()
result = before_send(event, hint)
# Assert that the event was dropped (None returned)
assert result is None
@@ -0,0 +1,100 @@
import pytest
from rest_framework.exceptions import ValidationError
from api.v1.serializer_utils.integrations import S3ConfigSerializer
class TestS3ConfigSerializer:
"""Test cases for S3ConfigSerializer validation."""
def test_validate_output_directory_valid_paths(self):
"""Test that valid output directory paths are accepted."""
serializer = S3ConfigSerializer()
# Test normal paths
assert serializer.validate_output_directory("test") == "test"
assert serializer.validate_output_directory("test/folder") == "test/folder"
assert serializer.validate_output_directory("my-folder_123") == "my-folder_123"
# Test paths with leading slashes (should be normalized)
assert serializer.validate_output_directory("/test") == "test"
assert serializer.validate_output_directory("/test/folder") == "test/folder"
# Test paths with excessive slashes (should be normalized)
assert serializer.validate_output_directory("///test") == "test"
assert serializer.validate_output_directory("///////test") == "test"
assert serializer.validate_output_directory("test//folder") == "test/folder"
assert serializer.validate_output_directory("test///folder") == "test/folder"
def test_validate_output_directory_empty_values(self):
"""Test that empty values raise validation errors."""
serializer = S3ConfigSerializer()
with pytest.raises(
ValidationError, match="Output directory cannot be empty or just"
):
serializer.validate_output_directory(".")
with pytest.raises(
ValidationError, match="Output directory cannot be empty or just"
):
serializer.validate_output_directory("/")
def test_validate_output_directory_invalid_characters(self):
"""Test that invalid characters are rejected."""
serializer = S3ConfigSerializer()
invalid_chars = ["<", ">", ":", '"', "|", "?", "*"]
for char in invalid_chars:
with pytest.raises(
ValidationError, match="Output directory contains invalid characters"
):
serializer.validate_output_directory(f"test{char}folder")
def test_validate_output_directory_too_long(self):
"""Test that paths that are too long are rejected."""
serializer = S3ConfigSerializer()
# Create a path longer than 900 characters
long_path = "a" * 901
with pytest.raises(ValidationError, match="Output directory path is too long"):
serializer.validate_output_directory(long_path)
def test_validate_output_directory_edge_cases(self):
"""Test edge cases for output directory validation."""
serializer = S3ConfigSerializer()
# Test path at the limit (900 characters)
path_at_limit = "a" * 900
assert serializer.validate_output_directory(path_at_limit) == path_at_limit
# Test complex normalization
assert serializer.validate_output_directory("//test/../folder//") == "folder"
assert serializer.validate_output_directory("/test/./folder/") == "test/folder"
def test_s3_config_serializer_full_validation(self):
"""Test the full S3ConfigSerializer with valid data."""
data = {
"bucket_name": "my-test-bucket",
"output_directory": "///////test", # This should be normalized
}
serializer = S3ConfigSerializer(data=data)
assert serializer.is_valid()
validated_data = serializer.validated_data
assert validated_data["bucket_name"] == "my-test-bucket"
assert validated_data["output_directory"] == "test" # Normalized
def test_s3_config_serializer_invalid_data(self):
"""Test the full S3ConfigSerializer with invalid data."""
data = {
"bucket_name": "my-test-bucket",
"output_directory": "test<invalid", # Contains invalid character
}
serializer = S3ConfigSerializer(data=data)
assert not serializer.is_valid()
assert "output_directory" in serializer.errors
+322 -5
View File
@@ -6,16 +6,18 @@ from rest_framework.exceptions import NotFound, ValidationError
from api.db_router import MainRouter
from api.exceptions import InvitationTokenExpiredException
from api.models import Invitation, Provider
from api.models import Integration, Invitation, Provider
from api.utils import (
get_prowler_provider_kwargs,
initialize_prowler_provider,
merge_dicts,
prowler_integration_connection_test,
prowler_provider_connection_test,
return_prowler_provider,
validate_invitation,
)
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
@@ -131,6 +133,21 @@ class TestInitializeProwlerProvider:
initialize_prowler_provider(provider)
mock_return_prowler_provider.return_value.assert_called_once_with(key="value")
@patch("api.utils.return_prowler_provider")
def test_initialize_prowler_provider_with_mutelist(
self, mock_return_prowler_provider
):
provider = MagicMock()
provider.secret.secret = {"key": "value"}
mutelist_processor = MagicMock()
mutelist_processor.configuration = {"Mutelist": {"key": "value"}}
mock_return_prowler_provider.return_value = MagicMock()
initialize_prowler_provider(provider, mutelist_processor)
mock_return_prowler_provider.return_value.assert_called_once_with(
key="value", mutelist_content={"key": "value"}
)
class TestProwlerProviderConnectionTest:
@patch("api.utils.return_prowler_provider")
@@ -182,6 +199,10 @@ class TestGetProwlerProviderKwargs:
Provider.ProviderChoices.M365.value,
{},
),
(
Provider.ProviderChoices.GITHUB.value,
{"organizations": ["provider_uid"]},
),
],
)
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
@@ -200,6 +221,25 @@ class TestGetProwlerProviderKwargs:
expected_result = {**secret_dict, **expected_extra_kwargs}
assert result == expected_result
def test_get_prowler_provider_kwargs_with_mutelist(self):
provider_uid = "provider_uid"
secret_dict = {"key": "value"}
secret_mock = MagicMock()
secret_mock.secret = secret_dict
mutelist_processor = MagicMock()
mutelist_processor.configuration = {"Mutelist": {"key": "value"}}
provider = MagicMock()
provider.provider = Provider.ProviderChoices.AWS.value
provider.secret = secret_mock
provider.uid = provider_uid
result = get_prowler_provider_kwargs(provider, mutelist_processor)
expected_result = {**secret_dict, "mutelist_content": {"key": "value"}}
assert result == expected_result
def test_get_prowler_provider_kwargs_unsupported_provider(self):
# Setup
provider_uid = "provider_uid"
@@ -254,7 +294,7 @@ class TestValidateInvitation:
assert result == invitation
mock_db.get.assert_called_once_with(
token="VALID_TOKEN", email="user@example.com"
token="VALID_TOKEN", email__iexact="user@example.com"
)
def test_invitation_not_found_raises_validation_error(self):
@@ -269,7 +309,7 @@ class TestValidateInvitation:
"invitation_token": "Invalid invitation code."
}
mock_db.get.assert_called_once_with(
token="INVALID_TOKEN", email="user@example.com"
token="INVALID_TOKEN", email__iexact="user@example.com"
)
def test_invitation_not_found_raises_not_found(self):
@@ -284,7 +324,7 @@ class TestValidateInvitation:
assert exc_info.value.detail == "Invitation is not valid."
mock_db.get.assert_called_once_with(
token="INVALID_TOKEN", email="user@example.com"
token="INVALID_TOKEN", email__iexact="user@example.com"
)
def test_invitation_expired(self, invitation):
@@ -332,5 +372,282 @@ class TestValidateInvitation:
"invitation_token": "Invalid invitation code."
}
mock_db.get.assert_called_once_with(
token="VALID_TOKEN", email="different@example.com"
token="VALID_TOKEN", email__iexact="different@example.com"
)
def test_valid_invitation_uppercase_email(self):
"""Test that validate_invitation works with case-insensitive email lookup."""
uppercase_email = "USER@example.com"
invitation = MagicMock(spec=Invitation)
invitation.token = "VALID_TOKEN"
invitation.email = uppercase_email
invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1)
invitation.state = Invitation.State.PENDING
invitation.tenant = MagicMock()
with patch("api.utils.Invitation.objects.using") as mock_using:
mock_db = mock_using.return_value
mock_db.get.return_value = invitation
result = validate_invitation("VALID_TOKEN", "user@example.com")
assert result == invitation
mock_db.get.assert_called_once_with(
token="VALID_TOKEN", email__iexact="user@example.com"
)
class TestProwlerIntegrationConnectionTest:
"""Test prowler_integration_connection_test function for SecurityHub regions reset."""
@patch("api.utils.SecurityHub")
def test_security_hub_connection_failure_resets_regions(
self, mock_security_hub_class
):
"""Test that SecurityHub connection failure resets regions to empty dict."""
# Create integration with existing regions configuration
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB
integration.credentials = {
"aws_access_key_id": "test_key",
"aws_secret_access_key": "test_secret",
}
integration.configuration = {
"send_only_fails": True,
"regions": {
"us-east-1": True,
"us-west-2": True,
"eu-west-1": False,
"ap-south-1": False,
},
}
# Mock provider relationship
mock_provider = MagicMock()
mock_provider.uid = "123456789012"
mock_relationship = MagicMock()
mock_relationship.provider = mock_provider
integration.integrationproviderrelationship_set.first.return_value = (
mock_relationship
)
# Mock failed SecurityHub connection
mock_connection = SecurityHubConnection(
is_connected=False,
error=Exception("SecurityHub testing"),
enabled_regions=set(),
disabled_regions=set(),
)
mock_security_hub_class.test_connection.return_value = mock_connection
# Call the function
result = prowler_integration_connection_test(integration)
# Assertions
assert result.is_connected is False
assert str(result.error) == "SecurityHub testing"
# Verify regions were completely reset to empty dict
assert integration.configuration["regions"] == {}
# Verify save was called to persist the change
integration.save.assert_called_once()
# Verify test_connection was called with correct parameters
mock_security_hub_class.test_connection.assert_called_once_with(
aws_account_id="123456789012",
raise_on_exception=False,
aws_access_key_id="test_key",
aws_secret_access_key="test_secret",
)
@patch("api.utils.SecurityHub")
def test_security_hub_connection_success_saves_regions(
self, mock_security_hub_class
):
"""Test that successful SecurityHub connection saves regions correctly."""
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.AWS_SECURITY_HUB
integration.credentials = {
"aws_access_key_id": "valid_key",
"aws_secret_access_key": "valid_secret",
}
integration.configuration = {"send_only_fails": False}
# Mock provider relationship
mock_provider = MagicMock()
mock_provider.uid = "123456789012"
mock_relationship = MagicMock()
mock_relationship.provider = mock_provider
integration.integrationproviderrelationship_set.first.return_value = (
mock_relationship
)
# Mock successful SecurityHub connection with regions
mock_connection = SecurityHubConnection(
is_connected=True,
error=None,
enabled_regions={"us-east-1", "eu-west-1"},
disabled_regions={"ap-south-1"},
)
mock_security_hub_class.test_connection.return_value = mock_connection
result = prowler_integration_connection_test(integration)
assert result.is_connected is True
# Verify regions were saved correctly
assert integration.configuration["regions"]["us-east-1"] is True
assert integration.configuration["regions"]["eu-west-1"] is True
assert integration.configuration["regions"]["ap-south-1"] is False
integration.save.assert_called_once()
@patch("api.utils.rls_transaction")
@patch("api.utils.Jira")
def test_jira_connection_success_basic_auth(
self, mock_jira_class, mock_rls_transaction
):
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.JIRA
integration.tenant_id = "test-tenant-id"
integration.credentials = {
"user_mail": "test@example.com",
"api_token": "test_api_token",
"domain": "example.atlassian.net",
}
integration.configuration = {}
# Mock successful JIRA connection with projects
mock_connection = MagicMock()
mock_connection.is_connected = True
mock_connection.error = None
mock_connection.projects = {"PROJ1": "Project 1", "PROJ2": "Project 2"}
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
mock_rls_transaction.return_value.__enter__ = MagicMock()
mock_rls_transaction.return_value.__exit__ = MagicMock()
result = prowler_integration_connection_test(integration)
assert result.is_connected is True
assert result.error is None
# Verify JIRA connection was called with correct parameters including domain from credentials
mock_jira_class.test_connection.assert_called_once_with(
user_mail="test@example.com",
api_token="test_api_token",
domain="example.atlassian.net",
raise_on_exception=False,
)
# Verify rls_transaction was called with correct tenant_id
mock_rls_transaction.assert_called_once_with("test-tenant-id")
# Verify projects were saved to integration configuration
assert integration.configuration["projects"] == {
"PROJ1": "Project 1",
"PROJ2": "Project 2",
}
# Verify integration.save() was called
integration.save.assert_called_once()
@patch("api.utils.rls_transaction")
@patch("api.utils.Jira")
def test_jira_connection_failure_invalid_credentials(
self, mock_jira_class, mock_rls_transaction
):
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.JIRA
integration.tenant_id = "test-tenant-id"
integration.credentials = {
"user_mail": "invalid@example.com",
"api_token": "invalid_token",
"domain": "invalid.atlassian.net",
}
integration.configuration = {}
# Mock failed JIRA connection
mock_connection = MagicMock()
mock_connection.is_connected = False
mock_connection.error = Exception("Authentication failed: Invalid credentials")
mock_connection.projects = {} # Empty projects when connection fails
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
mock_rls_transaction.return_value.__enter__ = MagicMock()
mock_rls_transaction.return_value.__exit__ = MagicMock()
result = prowler_integration_connection_test(integration)
assert result.is_connected is False
assert "Authentication failed: Invalid credentials" in str(result.error)
# Verify JIRA connection was called with correct parameters
mock_jira_class.test_connection.assert_called_once_with(
user_mail="invalid@example.com",
api_token="invalid_token",
domain="invalid.atlassian.net",
raise_on_exception=False,
)
# Verify rls_transaction was called even on failure
mock_rls_transaction.assert_called_once_with("test-tenant-id")
# Verify empty projects dict was saved to integration configuration
assert integration.configuration["projects"] == {}
# Verify integration.save() was called even on connection failure
integration.save.assert_called_once()
@patch("api.utils.rls_transaction")
@patch("api.utils.Jira")
def test_jira_connection_projects_update_with_existing_configuration(
self, mock_jira_class, mock_rls_transaction
):
"""Test that projects are properly updated when integration already has configuration data"""
integration = MagicMock()
integration.integration_type = Integration.IntegrationChoices.JIRA
integration.tenant_id = "test-tenant-id"
integration.credentials = {
"user_mail": "test@example.com",
"api_token": "test_api_token",
"domain": "example.atlassian.net",
}
integration.configuration = {
"issue_types": ["Task"], # Existing configuration
"projects": {"OLD_PROJ": "Old Project"}, # Will be overwritten
}
# Mock successful JIRA connection with new projects
mock_connection = MagicMock()
mock_connection.is_connected = True
mock_connection.error = None
mock_connection.projects = {
"NEW_PROJ1": "New Project 1",
"NEW_PROJ2": "New Project 2",
}
mock_jira_class.test_connection.return_value = mock_connection
# Mock rls_transaction context manager
mock_rls_transaction.return_value.__enter__ = MagicMock()
mock_rls_transaction.return_value.__exit__ = MagicMock()
result = prowler_integration_connection_test(integration)
assert result.is_connected is True
assert result.error is None
# Verify projects were updated (old projects replaced with new ones)
assert integration.configuration["projects"] == {
"NEW_PROJ1": "New Project 1",
"NEW_PROJ2": "New Project 2",
}
# Verify other configuration fields were preserved
assert integration.configuration["issue_types"] == ["Task"]
# Verify integration.save() was called
integration.save.assert_called_once()
File diff suppressed because it is too large Load Diff
+132 -9
View File
@@ -6,13 +6,18 @@ from django.db.models import Subquery
from rest_framework.exceptions import NotFound, ValidationError
from api.db_router import MainRouter
from api.db_utils import rls_transaction
from api.exceptions import InvitationTokenExpiredException
from api.models import Invitation, Provider, Resource
from api.models import Integration, Invitation, Processor, Provider, Resource
from api.v1.serializers import FindingMetadataSerializer
from prowler.lib.outputs.jira.jira import Jira, JiraBasicAuthError
from prowler.providers.aws.aws_provider import AwsProvider
from prowler.providers.aws.lib.s3.s3 import S3
from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.common.models import Connection
from prowler.providers.gcp.gcp_provider import GcpProvider
from prowler.providers.github.github_provider import GithubProvider
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
from prowler.providers.m365.m365_provider import M365Provider
@@ -55,14 +60,21 @@ def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
def return_prowler_provider(
provider: Provider,
) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider]:
) -> [
AwsProvider
| AzureProvider
| GcpProvider
| GithubProvider
| KubernetesProvider
| M365Provider
]:
"""Return the Prowler provider class based on the given provider type.
Args:
provider (Provider): The provider object containing the provider type and associated secrets.
Returns:
AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: The corresponding provider class.
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: The corresponding provider class.
Raises:
ValueError: If the provider type specified in `provider.provider` is not supported.
@@ -78,16 +90,21 @@ def return_prowler_provider(
prowler_provider = KubernetesProvider
case Provider.ProviderChoices.M365.value:
prowler_provider = M365Provider
case Provider.ProviderChoices.GITHUB.value:
prowler_provider = GithubProvider
case _:
raise ValueError(f"Provider type {provider.provider} not supported")
return prowler_provider
def get_prowler_provider_kwargs(provider: Provider) -> dict:
def get_prowler_provider_kwargs(
provider: Provider, mutelist_processor: Processor | None = None
) -> dict:
"""Get the Prowler provider kwargs based on the given provider type.
Args:
provider (Provider): The provider object containing the provider type and associated secret.
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
dict: The provider kwargs for the corresponding provider class.
@@ -105,24 +122,45 @@ def get_prowler_provider_kwargs(provider: Provider) -> dict:
}
elif provider.provider == Provider.ProviderChoices.KUBERNETES.value:
prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid}
elif provider.provider == Provider.ProviderChoices.GITHUB.value:
if provider.uid:
prowler_provider_kwargs = {
**prowler_provider_kwargs,
"organizations": [provider.uid],
}
if mutelist_processor:
mutelist_content = mutelist_processor.configuration.get("Mutelist", {})
if mutelist_content:
prowler_provider_kwargs["mutelist_content"] = mutelist_content
return prowler_provider_kwargs
def initialize_prowler_provider(
provider: Provider,
) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider:
mutelist_processor: Processor | None = None,
) -> (
AwsProvider
| AzureProvider
| GcpProvider
| GithubProvider
| KubernetesProvider
| M365Provider
):
"""Initialize a Prowler provider instance based on the given provider type.
Args:
provider (Provider): The provider object containing the provider type and associated secrets.
mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration.
Returns:
AwsProvider | AzureProvider | GcpProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `KubernetesProvider` or `M365Provider`) initialized with the
AwsProvider | AzureProvider | GcpProvider | GithubProvider | KubernetesProvider | M365Provider: An instance of the corresponding provider class
(`AwsProvider`, `AzureProvider`, `GcpProvider`, `GithubProvider`, `KubernetesProvider` or `M365Provider`) initialized with the
provider's secrets.
"""
prowler_provider = return_prowler_provider(provider)
prowler_provider_kwargs = get_prowler_provider_kwargs(provider)
prowler_provider_kwargs = get_prowler_provider_kwargs(provider, mutelist_processor)
return prowler_provider(**prowler_provider_kwargs)
@@ -147,6 +185,77 @@ def prowler_provider_connection_test(provider: Provider) -> Connection:
)
def prowler_integration_connection_test(integration: Integration) -> Connection:
"""Test the connection to a Prowler integration based on the given integration type.
Args:
integration (Integration): The integration object containing the integration type and associated credentials.
Returns:
Connection: A connection object representing the result of the connection test for the specified integration.
"""
if integration.integration_type == Integration.IntegrationChoices.AMAZON_S3:
return S3.test_connection(
**integration.credentials,
bucket_name=integration.configuration["bucket_name"],
raise_on_exception=False,
)
# TODO: It is possible that we can unify the connection test for all integrations, but need refactoring
# to avoid code duplication. Actually the AWS integrations are similar, so SecurityHub and S3 can be unified
# making some changes in the SDK.
elif (
integration.integration_type == Integration.IntegrationChoices.AWS_SECURITY_HUB
):
# Get the provider associated with this integration
provider_relationship = integration.integrationproviderrelationship_set.first()
if not provider_relationship:
return Connection(
is_connected=False, error="No provider associated with this integration"
)
credentials = (
integration.credentials
if integration.credentials
else provider_relationship.provider.secret.secret
)
connection = SecurityHub.test_connection(
aws_account_id=provider_relationship.provider.uid,
raise_on_exception=False,
**credentials,
)
# Only save regions if connection is successful
if connection.is_connected:
regions_status = {r: True for r in connection.enabled_regions}
regions_status.update({r: False for r in connection.disabled_regions})
# Save regions information in the integration configuration
integration.configuration["regions"] = regions_status
integration.save()
else:
# Reset regions information if connection fails
integration.configuration["regions"] = {}
integration.save()
return connection
elif integration.integration_type == Integration.IntegrationChoices.JIRA:
jira_connection = Jira.test_connection(
**integration.credentials,
raise_on_exception=False,
)
project_keys = jira_connection.projects if jira_connection.is_connected else {}
with rls_transaction(str(integration.tenant_id)):
integration.configuration["projects"] = project_keys
integration.save()
return jira_connection
elif integration.integration_type == Integration.IntegrationChoices.SLACK:
pass
else:
raise ValueError(
f"Integration type {integration.integration_type} not supported"
)
def validate_invitation(
invitation_token: str, email: str, raise_not_found=False
) -> Invitation:
@@ -187,7 +296,7 @@ def validate_invitation(
# Admin DB connector is used to bypass RLS protection since the invitation belongs to a tenant the user
# is not a member of yet
invitation = Invitation.objects.using(MainRouter.admin_db).get(
token=invitation_token, email=email
token=invitation_token, email__iexact=email
)
except Invitation.DoesNotExist:
if raise_not_found:
@@ -238,3 +347,17 @@ def get_findings_metadata_no_aggregations(tenant_id: str, filtered_queryset):
serializer.is_valid(raise_exception=True)
return serializer.data
def initialize_prowler_integration(integration: Integration) -> Jira:
# TODO Refactor other integrations to use this function
if integration.integration_type == Integration.IntegrationChoices.JIRA:
try:
return Jira(**integration.credentials)
except JiraBasicAuthError as jira_auth_error:
with rls_transaction(str(integration.tenant_id)):
integration.configuration["projects"] = {}
integration.connected = False
integration.connection_last_checked_at = datetime.now(tz=timezone.utc)
integration.save()
raise jira_auth_error
+14 -2
View File
@@ -24,20 +24,32 @@ class PaginateByPkMixin:
request, # noqa: F841
base_queryset,
manager,
select_related: list[str] | None = None,
prefetch_related: list[str] | None = None,
select_related: list | None = None,
prefetch_related: list | None = None,
) -> Response:
"""
Paginate a queryset by primary key.
This method is useful when you want to paginate a queryset that has been
filtered or annotated in a way that would be lost if you used the default
pagination method.
"""
pk_list = base_queryset.values_list("id", flat=True)
page = self.paginate_queryset(pk_list)
if page is None:
return Response(self.get_serializer(base_queryset, many=True).data)
queryset = manager.filter(id__in=page)
if select_related:
queryset = queryset.select_related(*select_related)
if prefetch_related:
queryset = queryset.prefetch_related(*prefetch_related)
# Optimize tags loading, if applicable
if hasattr(self, "_optimize_tags_loading"):
queryset = self._optimize_tags_loading(queryset)
queryset = sorted(queryset, key=lambda obj: page.index(obj.id))
serialized = self.get_serializer(queryset, many=True).data
@@ -0,0 +1,23 @@
import yaml
from rest_framework_json_api import serializers
from rest_framework_json_api.serializers import ValidationError
class BaseValidateSerializer(serializers.Serializer):
def validate(self, data):
if hasattr(self, "initial_data"):
initial_data = set(self.initial_data.keys()) - {"id", "type"}
unknown_keys = initial_data - set(self.fields.keys())
if unknown_keys:
raise ValidationError(f"Invalid fields: {unknown_keys}")
return data
class YamlOrJsonField(serializers.JSONField):
def to_internal_value(self, data):
if isinstance(data, str):
try:
data = yaml.safe_load(data)
except yaml.YAMLError as exc:
raise serializers.ValidationError("Invalid YAML format") from exc
return super().to_internal_value(data)
@@ -1,24 +1,78 @@
import os
import re
from drf_spectacular.utils import extend_schema_field
from rest_framework_json_api import serializers
from rest_framework_json_api.serializers import ValidationError
class BaseValidateSerializer(serializers.Serializer):
def validate(self, data):
if hasattr(self, "initial_data"):
initial_data = set(self.initial_data.keys()) - {"id", "type"}
unknown_keys = initial_data - set(self.fields.keys())
if unknown_keys:
raise ValidationError(f"Invalid fields: {unknown_keys}")
return data
# Integrations
from api.v1.serializer_utils.base import BaseValidateSerializer
class S3ConfigSerializer(BaseValidateSerializer):
bucket_name = serializers.CharField()
output_directory = serializers.CharField()
output_directory = serializers.CharField(allow_blank=True)
def validate_output_directory(self, value):
"""
Validate the output_directory field to ensure it's a properly formatted path.
Prevents paths with excessive slashes like "///////test".
If empty, sets a default value.
"""
# If empty or None, set default value
if not value:
return "output"
# Normalize the path to remove excessive slashes
normalized_path = os.path.normpath(value)
# Remove leading slashes for S3 paths
if normalized_path.startswith("/"):
normalized_path = normalized_path.lstrip("/")
# Check for invalid characters or patterns
if re.search(r'[<>:"|?*]', normalized_path):
raise serializers.ValidationError(
'Output directory contains invalid characters. Avoid: < > : " | ? *'
)
# Check for empty path after normalization
if not normalized_path or normalized_path == ".":
raise serializers.ValidationError(
"Output directory cannot be empty or just '.' or '/'."
)
# Check for paths that are too long (S3 key limit is 1024 characters, leave some room for filename)
if len(normalized_path) > 900:
raise serializers.ValidationError(
"Output directory path is too long (max 900 characters)."
)
return normalized_path
class Meta:
resource_name = "integrations"
class SecurityHubConfigSerializer(BaseValidateSerializer):
send_only_fails = serializers.BooleanField(default=False)
archive_previous_findings = serializers.BooleanField(default=False)
regions = serializers.DictField(default=dict, read_only=True)
def to_internal_value(self, data):
validated_data = super().to_internal_value(data)
# Always initialize regions as empty dict
validated_data["regions"] = {}
return validated_data
class Meta:
resource_name = "integrations"
class JiraConfigSerializer(BaseValidateSerializer):
domain = serializers.CharField(read_only=True)
issue_types = serializers.ListField(
read_only=True, child=serializers.CharField(), default=["Task"]
)
projects = serializers.DictField(read_only=True)
class Meta:
resource_name = "integrations"
@@ -39,6 +93,15 @@ class AWSCredentialSerializer(BaseValidateSerializer):
resource_name = "integrations"
class JiraCredentialSerializer(BaseValidateSerializer):
user_mail = serializers.EmailField(required=True)
api_token = serializers.CharField(required=True)
domain = serializers.CharField(required=True)
class Meta:
resource_name = "integrations"
@extend_schema_field(
{
"oneOf": [
@@ -90,6 +153,27 @@ class AWSCredentialSerializer(BaseValidateSerializer):
},
},
},
{
"type": "object",
"title": "JIRA Credentials",
"properties": {
"user_mail": {
"type": "string",
"format": "email",
"description": "The email address of the JIRA user account.",
},
"api_token": {
"type": "string",
"description": "The API token for authentication with JIRA. This can be generated from your "
"Atlassian account settings.",
},
"domain": {
"type": "string",
"description": "The JIRA domain/instance URL (e.g., 'your-domain.atlassian.net').",
},
},
"required": ["user_mail", "api_token", "domain"],
},
]
}
)
@@ -110,10 +194,40 @@ class IntegrationCredentialField(serializers.JSONField):
},
"output_directory": {
"type": "string",
"description": "The directory path within the bucket where files will be saved.",
"description": "The directory path within the bucket where files will be saved. Optional - "
'defaults to "output" if not provided. Path will be normalized to remove '
'excessive slashes and invalid characters are not allowed (< > : " | ? *). '
"Maximum length is 900 characters.",
"maxLength": 900,
"pattern": '^[^<>:"|?*]+$',
"default": "output",
},
},
"required": ["bucket_name", "output_directory"],
"required": ["bucket_name"],
},
{
"type": "object",
"title": "AWS Security Hub",
"properties": {
"send_only_fails": {
"type": "boolean",
"default": False,
"description": "If true, only findings with status 'FAIL' will be sent to Security Hub.",
},
"archive_previous_findings": {
"type": "boolean",
"default": False,
"description": "If true, archives findings that are not present in the current execution.",
},
},
},
{
"type": "object",
"title": "JIRA",
"description": "JIRA integration does not accept any configuration in the payload. Leave it as an "
"empty JSON object (`{}`).",
"properties": {},
"additionalProperties": False,
},
]
}

Some files were not shown because too many files have changed in this diff Show More