Compare commits

...

204 Commits

Author SHA1 Message Date
Pablo Lara bc39f3565b feat: add changelog (#7141)
(cherry picked from commit 2ce2ca7c91)
2025-03-06 15:47:30 +00:00
Víctor Fernández Poyatos 926f449ae6 fix(overviews): manage overview exceptions and use batch_size with bulk (#7140) 2025-03-06 15:39:51 +01:00
César Arroba 646668c6ae chore(ui-gha): delete double quotes on prowler version (#7139) 2025-03-06 15:39:40 +01:00
Prowler Bot 8e6b92792b fix(groups): display uid if alias is missing (#7138)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-03-06 14:41:57 +01:00
Prowler Bot 65c081ce38 fix(credentials): adjust helper links to fit width (#7134)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-03-06 13:01:10 +01:00
Pepe Fagoaga 5600131d6a revert(findings): change uid from varchar to text (#7132) 2025-03-06 11:43:07 +01:00
César Arroba dbd271980f chore(ui-gha): add version prefix (#7125) 2025-03-05 16:34:42 +01:00
Víctor Fernández Poyatos ff532a899e fix(reports): Fix task kwargs and result (#7124) 2025-03-05 16:34:36 +01:00
César Arroba 0c9675ec70 chore(ui): add prowler version on build (#7120) 2025-03-05 16:34:26 +01:00
Pablo Lara d45eda2b2b feat(compliance): new compliance selector (#7118) 2025-03-05 16:34:07 +01:00
dependabot[bot] 0abcf80d19 chore(deps-dev): bump pytest from 8.3.4 to 8.3.5 (#7097)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 16:33:35 +01:00
Pablo Lara c0e10fd395 chore(ui): update label from 'Select a scan job' to 'Select a cloud p… (#7107) 2025-03-05 16:32:20 +01:00
Pepe Fagoaga a80e9b26a8 chore(api): Use Prowler from v5.4 (#7092) 2025-03-03 22:30:58 +05:45
Sergio Garcia 2a9cd57fb8 fix(deps): update vulnerable cryptography dependency (#6993) 2025-03-03 17:38:23 +01:00
Prowler Bot a0ad1a5f49 chore(deps): bump cryptography from 43.0.1 to 44.0.1 in /api (#7003)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-03-03 17:21:15 +01:00
César Arroba dff22dd166 Revert "chore(api): update prowler version to 5.4 (#7091)"
This reverts commit 58138810b9.
2025-03-03 17:11:20 +01:00
César Arroba 58138810b9 chore(api): update prowler version to 5.4 (#7091)
Co-authored-by: Prowler Bot <bot@prowler.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-03-03 17:08:45 +01:00
Víctor Fernández Poyatos 1ecc272fe4 chore: update changelog 2025-03-03 16:27:05 +01:00
Pablo Lara b784167006 fix(roles): show the correct error message (#7089) 2025-03-03 15:50:28 +01:00
Pablo Lara cf0ec8dea0 fix: bug with create role and unlimited visibility checkbox (#7088) 2025-03-03 15:50:23 +01:00
Sergio Garcia 96cae5e961 feat(aws): add fixers for threat detection checks (#7085) 2025-03-03 15:50:18 +01:00
Pablo Lara a48e5cb15f feat(version): add prowler version to the sidebar (#7086) 2025-03-03 15:50:14 +01:00
Pablo Lara 5a9ff007e0 chore: Update the latest table findings with the most recent changes (#7084) 2025-03-03 15:50:06 +01:00
Prowler Bot 24c45f894c chore(regions_update): Changes in regions for AWS services (#7034)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 15:47:09 +01:00
dependabot[bot] 5d03c85629 chore(deps): bump cryptography from 43.0.1 to 44.0.1 in /api (#7001)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 15:46:15 +01:00
Pablo Lara 41dc397a7a feat(sidebar): sidebar with new functionalities (#7018) 2025-03-03 15:01:38 +01:00
Prowler Bot 237a9adce9 chore(regions_update): Changes in regions for AWS services (#7067)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 15:01:29 +01:00
Sergio Garcia a06167f1c2 fix(threat detection): run single threat detection check (#7065) 2025-03-03 15:01:21 +01:00
Pepe Fagoaga a7d58c40dd refactor(stats): Use Finding instead of Check_Report (#7053)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2025-03-03 15:01:12 +01:00
Pepe Fagoaga e260c46389 chore(examples): Scan AWS (#7064) 2025-03-03 15:01:02 +01:00
Sergio Garcia 115169a596 chore(gcp): enhance GCP APIs logic (#7046) 2025-03-03 15:00:49 +01:00
dependabot[bot] 5b19173c1d chore(deps): bump trufflesecurity/trufflehog from 3.88.13 to 3.88.14 (#7063)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 15:00:40 +01:00
Daniel Barranquero d3dd1644e6 feat(m365): add sharepoint service with 4 checks (#7057)
Co-authored-by: MarioRgzLpz <mariorgzlpz1809@gmail.com>
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-03-03 15:00:29 +01:00
Pedro Martín 8ff0c59964 feat(docs): add info related with sts assume role and regions (#7062) 2025-03-03 15:00:19 +01:00
Daniel Barranquero 285939c389 fix(azure): handle account not supporting Blob (#7060) 2025-03-03 15:00:04 +01:00
Sergio Garcia a62ae8af51 fix(ecs): ensure unique finding id in ECS checks (#7059) 2025-03-03 14:59:56 +01:00
Prowler Bot 5d78b9e439 chore(regions_update): Changes in regions for AWS services (#7056)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:59:45 +01:00
Hugo Pereira Brito 1056c270ca feat(microsoft365): add new check entra_policy_ensure_default_user_cannot_create_tenants (#6918)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-03-03 14:59:35 +01:00
Pablo Lara eeef6600b7 feat(exports): download scan exports (#7006) 2025-03-03 14:59:14 +01:00
Pepe Fagoaga e142f17abe fix(env): UI version must be stable (#7055) 2025-03-03 14:58:45 +01:00
Víctor Fernández Poyatos a65d858dac fix(migrations): Fix migration dependency order (#7051) 2025-03-03 14:58:32 +01:00
Víctor Fernández Poyatos 6235a1ba41 feat(labeler): apply label on migration changes (#7052) 2025-03-03 14:58:22 +01:00
Pepe Fagoaga 05007d03ee fix(findings): change uid from varchar to text (#7048) 2025-03-03 14:58:13 +01:00
Víctor Fernández Poyatos 102d099947 feat(findings): Add Django management command to populate database with dummy data (#7049) 2025-03-03 14:58:03 +01:00
Adrián Jesús Peña Rodríguez 3194675a5c feat(export): add API export system (#6878) 2025-03-03 14:57:40 +01:00
dependabot[bot] 14e6e4aa68 chore(deps-dev): bump black from 24.10.0 to 25.1.0 (#6733)
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-03-03 14:57:22 +01:00
Pedro Martín b24c3665b5 feat(aws): add ISO 27001 2022 compliance framework (#7035)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-03-03 14:56:31 +01:00
Prowler Bot 1f60878867 chore(regions_update): Changes in regions for AWS services (#7015)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:54:16 +01:00
dependabot[bot] 2dd18662d8 chore(deps): bump google-api-python-client from 2.161.0 to 2.162.0 (#7037)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:54:02 +01:00
Hugo Pereira Brito 175360dbe6 refactor(microsoft365): CheckReportMicrosoft365 and resource metadata (#6952) 2025-03-03 14:53:42 +01:00
Víctor Fernández Poyatos 80e24b971f feat(findings): Optimize findings endpoint (#7019) 2025-03-03 14:53:22 +01:00
Pepe Fagoaga 78877c470a chore(action): Conventional Commit Check (#7033) 2025-03-03 14:53:08 +01:00
dependabot[bot] 9c9b100359 chore(deps): bump trufflesecurity/trufflehog from 3.88.12 to 3.88.13 (#7026)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:52:51 +01:00
Pedro Martín 10f3232294 feat(outputs): add sample outputs (#6945) 2025-03-03 14:52:37 +01:00
Pedro Martín a2e5f70f36 fix(cis): show report table on the CLI (#6979) 2025-03-03 14:52:23 +01:00
Pedro Martín 8d8b31c757 feat(azure): add PCI DSS 4.0 (#6982) 2025-03-03 14:52:03 +01:00
Pedro Martín cba1e718b9 feat(kubernetes): add PCI DSS 4.0 (#7013) 2025-03-03 14:51:42 +01:00
Pedro Martín 6c3c37fc26 feat(dashboard): take the latest finding uid by timestamp (#6987) 2025-03-03 14:51:29 +01:00
Víctor Fernández Poyatos b610cacd0c feat(tasks): add deletion queue for deletion tasks (#7022) 2025-03-03 14:51:14 +01:00
Pedro Martín 027a5705cb feat(gcp): add PCI DSS 4.0 (#7010) 2025-03-03 14:51:00 +01:00
Prowler Bot b7fbfb4360 chore(regions_update): Changes in regions for AWS services (#7011)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:50:41 +01:00
dependabot[bot] 5acf0a7e3d chore(deps-dev): bump mkdocs-material from 9.6.4 to 9.6.5 (#7007)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:50:26 +01:00
Raj Chowdhury 3a25e86e30 fix(typo): solve typo in dashboard.md (#7009) 2025-03-03 14:50:06 +01:00
dependabot[bot] 50f1592eb3 chore(deps): bump trufflesecurity/trufflehog from 3.88.11 to 3.88.12 (#7008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:49:46 +01:00
César Arroba 0f2927cb88 feat(api): setup sentry for OSS API (#6874) 2025-03-03 14:49:32 +01:00
Pablo Lara b4e1434052 chore(users): renaming the account now triggers a re-render in the sidebar (#7005) 2025-03-03 14:49:16 +01:00
dependabot[bot] 43710783f9 chore(deps): bump python from 3.12.8-alpine3.20 to 3.12.9-alpine3.20 (#6882)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:49:04 +01:00
dependabot[bot] 16f767e7b9 chore(deps): bump tzlocal from 5.2 to 5.3 (#6932)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:48:41 +01:00
Hugo Pereira Brito 42818217a0 docs(tutorials): update all deprecated poetry shell references (#7002) 2025-03-03 14:44:50 +01:00
Prowler Bot 13405594b2 chore(regions_update): Changes in regions for AWS services (#6998)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:44:33 +01:00
Pedro Martín b5a3852334 chore(github): add compliance to PR labeler (#6996) 2025-03-03 14:44:16 +01:00
Hugo Pereira Brito 181ff1acb3 docs(installation): add warning for poetry shell deprecation in README (#6983) 2025-03-03 14:43:35 +01:00
Pablo Lara 91e59a3279 chore(findings): add 'Status Extended' attribute to finding details (#6997) 2025-03-03 14:43:20 +01:00
Pedro Martín b3fad1a765 feat(aws): add PCI DSS 4.0 (#6949) 2025-03-03 14:42:41 +01:00
dependabot[bot] 6f90927a79 chore(deps): bump trufflesecurity/trufflehog from 3.88.9 to 3.88.11 (#6988)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:42:24 +01:00
dependabot[bot] 0bb4a9a3e9 chore(deps): bump kubernetes from 32.0.0 to 32.0.1 (#6992)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:42:10 +01:00
Pablo Lara 80d9cde60b feat(scans): update the progress for executing scans (#6972) 2025-03-03 14:41:39 +01:00
César Arroba 11196c2f83 chore(gha): trigger API or UI deployment when push to master (#6946) 2025-03-03 14:41:21 +01:00
Prowler Bot 55a0d0a1b5 chore(regions_update): Changes in regions for AWS services (#6978) 2025-03-03 14:41:06 +01:00
Pedro Martín 4e5e1d7bd4 feat(aws): add compliance CIS 4.0 (#6937) 2025-03-03 14:40:17 +01:00
dependabot[bot] 06a0a434ab chore(deps-dev): bump flake8 from 7.1.1 to 7.1.2 (#6954)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:39:58 +01:00
Pepe Fagoaga 153833fc55 fix(ocsf): Adapt for 1.4.0 (#6971) 2025-03-03 14:38:57 +01:00
Prowler Bot fc4877975f chore(regions_update): Changes in regions for AWS services (#6968)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:38:21 +01:00
dependabot[bot] 0797efd4fd chore(deps-dev): bump bandit from 1.8.2 to 1.8.3 (#6955)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:38:07 +01:00
Prowler Bot fbec99a0b7 chore(regions_update): Changes in regions for AWS services (#6944)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:37:28 +01:00
dependabot[bot] b2cb1de95e chore(deps): bump trufflesecurity/trufflehog from 3.88.8 to 3.88.9 (#6943)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:37:06 +01:00
dependabot[bot] 190c2316d7 chore(deps): bump google-api-python-client from 2.160.0 to 2.161.0 (#6933)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:36:16 +01:00
César Arroba f6c352281a fix(gha): fix short sha step (#6939) 2025-03-03 14:36:00 +01:00
César Arroba 66dfe89936 chore(gha): add tag for api and ui images on push to master (#6920) 2025-03-03 14:35:41 +01:00
dependabot[bot] 8b3942ca49 chore(deps): bump trufflesecurity/trufflehog from 3.88.7 to 3.88.8 (#6931)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:35:06 +01:00
dependabot[bot] 9d35213bd5 chore(deps-dev): bump mkdocs-material from 9.6.3 to 9.6.4 (#6913)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:34:13 +01:00
Víctor Fernández Poyatos 3e586e615d feat(social-login): Add social login integration for Google and Github OAuth providers (#6906) 2025-03-03 14:33:12 +01:00
Sergio Garcia a4f950e093 chore(docs): external K8s cluster Prowler App credentials (#6921) 2025-03-03 14:32:30 +01:00
Pedro Martín 7c2441f6ff fix(gcp): remove typos on CIS 3.0 (#6917) 2025-03-03 14:31:48 +01:00
dependabot[bot] 0a92af3eb2 chore(deps): bump trufflesecurity/trufflehog from 3.88.6 to 3.88.7 (#6915)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:31:25 +01:00
César Arroba 666f3a0e20 fix(gha): fix test build containers on pull requests actions (#6909) 2025-03-03 14:30:35 +01:00
dependabot[bot] 06ef98b5cc chore(deps-dev): bump coverage from 7.6.11 to 7.6.12 (#6897)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:29:39 +01:00
Prowler Bot 79125bdd40 chore(regions_update): Changes in regions for AWS services (#6900)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-03-03 14:28:42 +01:00
César Arroba e8e8b085ac chore(api): test build container image on pull request (#6850) 2025-03-03 14:28:24 +01:00
César Arroba c9b81d003a chore(ui): test build container image on pull request (#6849) 2025-03-03 14:27:33 +01:00
Pepe Fagoaga 23fa3c1e38 chore(version): Update version to 5.4.0 (#6894) 2025-03-03 14:26:51 +01:00
dependabot[bot] 03fbd0baca chore(deps-dev): bump coverage from 7.6.10 to 7.6.11 (#6887)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 14:24:50 +01:00
dependabot[bot] d4fe24ef47 chore(deps): bump trufflesecurity/trufflehog from 3.88.5 to 3.88.6 (#6888)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 13:05:51 +01:00
Prowler Bot 9c5220ee98 fix(elasticache): improve logic in elasticache_redis_cluster_backup_enabled (#7045)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-26 12:12:44 +01:00
Prowler Bot 6491bce5a6 fix(azure): migrate resource models to avoid using SDK defaults (#7043)
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2025-02-26 10:49:33 +01:00
Prowler Bot ca375dd79c chore(iam): enhance iam_role_cross_service_confused_deputy_prevention recommendation (#7041)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-02-26 08:35:26 +01:00
Prowler Bot e807573b54 fix(soc2_aws): remove duplicated checks (#6999)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-20 17:26:50 +05:30
Prowler Bot c0f4c9743f fix(deps): update vulnerable cryptography dependency (#6994)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-20 16:04:14 +05:30
Prowler Bot 5974d0b5da fix(report): remove invalid resources in report (#6984)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-20 11:05:58 +05:30
Prowler Bot 6244a8a5f7 fix(roles): handle empty response in deleteRole and ensure revalidation (#6977)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-02-19 09:06:56 +01:00
Prowler Bot 5b9dae4529 test(cloudfront): add name retrieval test for cloudfront bucket domains (#6975)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-02-19 09:12:17 +05:30
Prowler Bot a424374c44 fix(cloudfront): Incorrect bucket name retrievement (#6967)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-02-19 08:08:31 +05:30
Prowler Bot b7fc2542e8 fix(gcp): Correct false positive when sslMode=ENCRYPTED_ONLY in CloudSQL (#6942)
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2025-02-14 16:36:26 -05:00
Prowler Bot 83a1598a1e fix(issue pages): apply sorting by default in issue pages (#6935)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-02-14 10:35:49 +01:00
Prowler Bot b22b56a06b fix(gcp): handle DNS Managed Zone with no DNSSEC (#6928)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-13 14:50:05 -05:00
Prowler Bot 5020e4713c fix(aws): codebuild service threw KeyError for projects type CODEPIPELINE (#6930)
Co-authored-by: Kay Agahd <kagahd@users.noreply.github.com>
2025-02-13 13:53:05 -05:00
Prowler Bot ee534a740e fix(aws): SNS threw IndexError if SubscriptionArn is PendingConfirmation (#6923)
Co-authored-by: Kay Agahd <kagahd@users.noreply.github.com>
2025-02-13 10:35:19 -05:00
Prowler Bot 48cb45b7a8 fix(aws): handle AccessDenied when retrieving resource policy (#6912)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-12 19:15:51 -05:00
Prowler Bot 91b74822e9 fix(kms): Amazon KMS API call error handling (#6904)
Co-authored-by: Ogonna Iwunze <1915636+wunzeco@users.noreply.github.com>
2025-02-12 11:08:47 -05:00
Pepe Fagoaga 287eef5085 chore(version): Update version to 5.3.1 (#6895) 2025-02-12 16:39:09 +05:45
Mario Rodriguez Lopez 45d359c84a feat(microsof365): Add documentation and compliance file (#6195)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
2025-02-10 17:18:43 +01:00
Pablo Lara 6049e5d4e8 chore: Update prowler api version (#6877)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-02-10 17:39:08 +05:45
Víctor Fernández Poyatos dfd377f89e chore(api): Update changelog and specs (#6876) 2025-02-10 12:25:04 +01:00
Víctor Fernández Poyatos 37e6c52c14 chore: Add needed steps for API in PR template (#6875) 2025-02-10 12:24:51 +01:00
Pepe Fagoaga d6a7f4d88f fix(kubernetes): Change UID validation (#6869)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-10 11:24:34 +01:00
Pepe Fagoaga 239cda0a90 chore: Rename dashboard table latest findings (#6873)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-02-10 11:24:27 +01:00
dependabot[bot] 4a821e425b chore(deps-dev): bump mkdocs-material from 9.6.2 to 9.6.3 (#6871)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:24:21 +01:00
Sergio Garcia e1a2f0c204 docs(eks): add documentation about EKS onboarding (#6853)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-02-10 11:24:16 +01:00
Prowler Bot c70860c733 chore(regions_update): Changes in regions for AWS services (#6858)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-02-10 11:24:09 +01:00
Víctor Fernández Poyatos 05e71e033f feat(findings): Use ArrayAgg and subqueries on metadata endpoint (#6863)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-02-10 11:24:03 +01:00
Pablo Lara 5164ec2eb9 feat: implement new functionality with inserted_at__gte in findings a… (#6864) 2025-02-10 11:23:58 +01:00
Víctor Fernández Poyatos be18dac4f9 docs: Add details about user creation in Prowler app (#6862) 2025-02-10 11:23:52 +01:00
dependabot[bot] bb126c242f chore(deps): bump microsoft-kiota-abstractions from 1.9.1 to 1.9.2 (#6856)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:23:47 +01:00
Víctor Fernández Poyatos e27780a856 feat(findings): Require date filters for findings endpoints (#6800) 2025-02-10 11:23:41 +01:00
Pranay Girase 196ec51751 fix(typo): typos in Dashboard and Report in HTML (#6847) 2025-02-10 11:23:33 +01:00
Prowler Bot 86abf9e64c chore(regions_update): Changes in regions for AWS services (#6848)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-02-10 11:23:28 +01:00
dependabot[bot] 9d8be578e3 chore(deps): bump trufflesecurity/trufflehog from 3.88.4 to 3.88.5 (#6844)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:23:19 +01:00
Mario Rodriguez Lopez b3aa800082 feat(entra): add new check entra_thirdparty_integrated_apps_not_allowed (#6357)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:23:13 +01:00
Ogonna Iwunze 501674a778 feat(kms): add kms_cmk_not_multi_region AWS check (#6794)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:23:01 +01:00
Prowler Bot 5ff6ae79d8 chore(regions_update): Changes in regions for AWS services (#6821)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-02-10 11:21:08 +01:00
Mario Rodriguez Lopez e518a869ab feat(entra): add new entra service for Microsoft365 (#6326)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:20:58 +01:00
Mario Rodriguez Lopez 43927a62f3 feat(microsoft365): add new check admincenter_settings_password_never_expire (#6023)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:20:52 +01:00
dependabot[bot] 335980c8d8 chore(deps): bump kubernetes from 31.0.0 to 32.0.0 (#6678)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:20:42 +01:00
Pablo Lara ca3ee378db style(forms): improve spacing consistency (#6814) 2025-02-10 11:17:11 +01:00
Pablo Lara c05bc1068a chore(forms): improvements to the sign-in and sign-up forms (#6813) 2025-02-10 11:17:03 +01:00
Drew Kerrigan 2e3164636d docs(): add description of changed and new delta values to prowler app tutorial (#6801) 2025-02-10 11:16:56 +01:00
dependabot[bot] c34e07fc40 chore(deps): bump pytz from 2024.2 to 2025.1 (#6765)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:16:50 +01:00
dependabot[bot] 6022122a61 chore(deps-dev): bump mkdocs-material from 9.5.50 to 9.6.2 (#6799)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:16:44 +01:00
dependabot[bot] f65f5e4b46 chore(deps-dev): bump pylint from 3.3.3 to 3.3.4 (#6721)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:16:37 +01:00
Pablo Lara dee17733a0 feat(scans): show scan details right after launch (#6791) 2025-02-10 11:16:30 +01:00
dependabot[bot] cddda1e64e chore(deps): bump trufflesecurity/trufflehog from 3.88.2 to 3.88.4 (#6760)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:16:23 +01:00
dependabot[bot] f7b873db03 chore(deps): bump google-api-python-client from 2.159.0 to 2.160.0 (#6720)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:16:17 +01:00
Víctor Fernández Poyatos 792bc70d0a feat(schedules): Rework daily schedule to always show the next scan (#6700) 2025-02-10 11:16:11 +01:00
Hugo Pereira Brito 185491b061 fix: microsoft365 mutelist (#6724) 2025-02-10 11:16:05 +01:00
dependabot[bot] 3af8a43480 chore(deps): bump microsoft-kiota-abstractions from 1.6.8 to 1.9.1 (#6734)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:15:57 +01:00
Pablo Lara fd78406b29 fix: Enable hot reloading when using Docker Compose for UI (#6750) 2025-02-10 11:15:49 +01:00
Matt Johnson 4758b258a3 chore: Update Google Analytics ID across all docs.prowler.com sites. (#6730) 2025-02-10 11:15:41 +01:00
dependabot[bot] 015e2b3b88 chore(deps): bump uuid from 10.0.0 to 11.0.5 in /ui (#6516)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:15:33 +01:00
Mario Rodriguez Lopez e184c9cb61 feat(m365): add Microsoft 365 provider (#5902)
Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
Co-authored-by: HugoPBrito <hugopbrit@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:15:24 +01:00
dependabot[bot] 9004a01183 chore(deps): bump azure-mgmt-web from 7.3.1 to 8.0.0 (#6680)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:15:18 +01:00
dependabot[bot] dd65ba3d9e chore(deps): bump msgraph-sdk from 1.17.0 to 1.18.0 (#6679)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:15:08 +01:00
dependabot[bot] bba616a18f chore(deps): bump azure-storage-blob from 12.24.0 to 12.24.1 (#6666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 11:14:33 +01:00
Pepe Fagoaga aa0f8d2981 chore: bump for next minor (#6672) 2025-02-10 11:13:42 +01:00
Paolo Frigo 2511d6ffa9 docs: update # of checks, services, frameworks and categories (#6528)
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-10 11:12:02 +01:00
Prowler Bot 27329457be fix(dashboard): adjust the bar chart display (#6868)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-02-07 11:04:23 -05:00
Prowler Bot 7189f3d526 fix(aws): key error for detect-secrets (#6865)
Co-authored-by: Kay Agahd <kagahd@users.noreply.github.com>
2025-02-07 10:04:54 -05:00
Prowler Bot 58e7589c9d fix(kms): handle error in DescribeKey function (#6842)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-05 15:27:43 -05:00
Prowler Bot d60f4b5ded fix(cloudfront): fix false positive in s3 origins (#6838)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
2025-02-05 13:36:49 -05:00
Prowler Bot 4c2ec094f6 fix(findings): Spelling mistakes correction (#6834)
Co-authored-by: Gary Mclean <gary.mclean@krrv.io>
2025-02-05 11:53:17 -05:00
Prowler Bot 395ecaff5b fix(directoryservice): handle ClientException (#6828)
Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com>
2025-02-05 11:20:13 -05:00
Prowler Bot c39506ef7d fix(aws) wording of report.status_extended in awslambda_function_not_publicly_accessible (#6831)
Co-authored-by: Kay Agahd <kagahd@users.noreply.github.com>
2025-02-05 11:18:27 -05:00
Prowler Bot eb90d479e2 chore(aws_audit_manager_control_tower_guardrails): add checks to reqs (#6803)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-04 19:47:04 -05:00
Prowler Bot b92a73f5ea fix(elasticache): InvalidReplicationGroupStateFault error (#6820)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-02-04 16:08:26 -05:00
Prowler Bot ad121f3059 chore(deps-dev): bump moto from 5.0.27 to 5.0.28 (#6817)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-02-04 14:25:04 -05:00
Pepe Fagoaga 70e4c5a44e chore: bump for next patch (#6764) 2025-02-03 15:25:23 -05:00
Prowler Bot b5a46b7b59 fix(cis_1.5_aws): add checks to needed reqs (#6798)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-02-03 11:37:53 -05:00
Prowler Bot f1a97cd166 fix(cis_1.4_aws): add checks to needed reqs (#6796)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-02-03 11:37:39 -05:00
Prowler Bot 0774508093 fix(cis_2.0_aws): add checks to needed reqs (#6787)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-02-03 11:37:24 -05:00
Prowler Bot 0664ce6b94 fix(gcp): fix wrong provider value in check (#6789)
Co-authored-by: secretcod3r <101349794+secretcod3r@users.noreply.github.com>
2025-02-03 10:32:53 -05:00
Prowler Bot 407c779c52 fix(findings): remove default status filtering (#6785)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-02-03 15:25:11 +01:00
Prowler Bot c60f13f23f fix(findings): order findings by inserted_at DESC (#6783)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-02-03 11:57:45 +01:00
Prowler Bot 37d912ef01 fix(celery): Kill celery worker process after every task to release memory (#6763)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-31 19:37:34 +05:45
Pepe Fagoaga d3de89c017 chore: bump for next patch (#6762) 2025-01-31 19:34:54 +05:45
Prowler Bot cb22af25c6 fix(db_event): Handle other events (#6757)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 21:48:42 +05:45
Prowler Bot a534b94495 fix(set_report_color): Add more details to error (#6755)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 21:46:34 +05:45
Prowler Bot 6262b4ff0b feat(scans): Optimize read queries during scans (#6756)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-30 20:56:11 +05:45
Prowler Bot 84ecd7ab2c feat(findings): Improve /findings/metadata performance (#6749)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-30 18:39:25 +05:45
Prowler Bot 1a5428445a fix(neptune): correct service name (#6747)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 17:52:00 +05:45
Prowler Bot ac8e991ca0 fix(aws): iam_user_with_temporary_credentials resource in OCSF (#6741)
Co-authored-by: Kay Agahd <kagahd@users.noreply.github.com>
2025-01-30 17:17:20 +05:45
Prowler Bot 83a0331472 fix(acm): Key Error DomainName (#6744)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 17:17:05 +05:45
Prowler Bot cce31e2971 fix(finding): raise when generating invalid findings (#6745)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 17:16:50 +05:45
Prowler Bot 0adf7d6e77 fix(sns): Add region to subscriptions (#6740)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-30 15:46:55 +05:45
Pepe Fagoaga 295f8b557e chore: bump for next patch (#6727) 2025-01-29 20:00:47 +05:45
Prowler Bot bb2c5c3161 fix(scans): change label for next scan (#6726)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-01-29 10:49:35 +01:00
Prowler Bot 0018f36a36 fix(migrations): Use indexes instead of constraints to define an index (#6723)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-29 14:29:03 +05:45
Prowler Bot 857de84f49 fix(defender): add field to SecurityContacts (#6715)
Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
2025-01-29 13:53:14 +05:45
Prowler Bot 9630f2242a revert: Update Django DB manager to use psycopg3 and connection pooling (#6719)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-28 22:33:32 +05:45
Prowler Bot 1fe125867c fix(scan-summaries): Improve efficiency on providers overview (#6718)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-28 22:15:22 +05:45
Prowler Bot 0737893240 fix(scans): filters and sorting for scan table (#6714)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-01-28 13:29:39 +01:00
Prowler Bot 282fe3d348 fix(scans, findings): Improve API performance ordering by inserted_at instead of id (#6712)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-28 17:28:26 +05:45
Prowler Bot b5d83640ae fix: fixed bug when opening finding details while a scan is in progress (#6709)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
2025-01-28 07:01:43 +01:00
Prowler Bot 2823d3ad21 fix(cloudsql): add trusted client certificates case for cloudsql_instance_ssl_connections (#6687)
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2025-01-24 12:19:05 -05:00
Prowler Bot 00b93bfe86 fix(cloudwatch): NoneType object is not iterable (#6677)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2025-01-23 13:25:02 -05:00
Pepe Fagoaga 84c253d887 chore: bump for next patch (#6673) 2025-01-23 13:13:05 -05:00
422 changed files with 112088 additions and 5439 deletions
+33 -5
View File
@@ -3,7 +3,7 @@
# For production, it is recommended to use a secure method to store these variables and change the default secret keys.
#### Prowler UI Configuration ####
PROWLER_UI_VERSION="latest"
PROWLER_UI_VERSION="stable"
SITE_URL=http://localhost:3000
API_BASE_URL=http://prowler-api:8080/api/v1
NEXT_PUBLIC_API_DOCS_URL=http://prowler-api:8080/api/v1/docs
@@ -30,6 +30,30 @@ VALKEY_HOST=valkey
VALKEY_PORT=6379
VALKEY_DB=0
# API scan settings
# The path to the directory where scan output should be stored
DJANGO_TMP_OUTPUT_DIRECTORY = "/tmp/prowler_api_output"
# The maximum number of findings to process in a single batch
DJANGO_FINDINGS_BATCH_SIZE = 1000
# The AWS access key to be used when uploading scan output to an S3 bucket
# If left empty, default AWS credentials resolution behavior will be used
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID=""
# The AWS secret key to be used when uploading scan output to an S3 bucket
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY=""
# An optional AWS session token
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN=""
# The AWS region where your S3 bucket is located (e.g., "us-east-1")
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION=""
# The name of the S3 bucket where scan output should be stored
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET=""
# Django settings
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
DJANGO_BIND_ADDRESS=0.0.0.0
@@ -92,7 +116,11 @@ jQIDAQAB
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4
DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10
DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000
DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400
DJANGO_SENTRY_DSN=
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
#### Prowler release version ####
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v5.4.0
+10
View File
@@ -92,3 +92,13 @@ component/api:
component/ui:
- changed-files:
- any-glob-to-any-file: "ui/**"
compliance:
- changed-files:
- any-glob-to-any-file: "prowler/compliance/**"
- any-glob-to-any-file: "prowler/lib/outputs/compliance/**"
- any-glob-to-any-file: "tests/lib/outputs/compliance/**"
review-django-migrations:
- changed-files:
- any-glob-to-any-file: "api/src/backend/api/migrations/**"
+5
View File
@@ -17,6 +17,11 @@ Please include a summary of the change and which issue is fixed. List any depend
- [ ] Review if backport is needed.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
#### API
- [ ] Verify if API specs need to be regenerated.
- [ ] Check if version updates are required (e.g., specs, Poetry, etc.).
- [ ] Ensure new entries are added to [CHANGELOG.md](https://github.com/prowler-cloud/prowler/blob/master/api/CHANGELOG.md), if applicable.
### License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
@@ -63,6 +63,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- 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@v3
with:
@@ -82,6 +88,7 @@ jobs:
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
@@ -96,3 +103,12 @@ jobs:
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trigger deployment
if: github.event_name == 'push'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.CLOUD_DISPATCH }}
event-type: prowler-api-deploy
client-payload: '{"sha": "${{ github.sha }}", "short_sha": "${{ env.SHORT_SHA }}"}'
+18 -2
View File
@@ -6,6 +6,7 @@ on:
- "master"
- "v5.*"
paths:
- ".github/workflows/api-pull-request.yml"
- "api/**"
pull_request:
branches:
@@ -14,7 +15,6 @@ on:
paths:
- "api/**"
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
@@ -26,7 +26,8 @@ env:
VALKEY_HOST: localhost
VALKEY_PORT: 6379
VALKEY_DB: 0
API_WORKING_DIR: ./api
IMAGE_NAME: prowler-api
jobs:
test:
@@ -171,3 +172,18 @@ jobs:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: api
test-container-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Container
uses: docker/build-push-action@v6
with:
context: ${{ env.API_WORKING_DIR }}
push: false
tags: ${{ env.IMAGE_NAME }}:latest
outputs: type=docker
cache-from: type=gha
cache-to: type=gha,mode=max
+23
View File
@@ -0,0 +1,23 @@
name: Prowler - Conventional Commit
on:
pull_request:
types:
- "opened"
- "edited"
- "synchronize"
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
jobs:
conventional-commit-check:
runs-on: ubuntu-latest
steps:
- name: conventional-commit-check
id: conventional-commit-check
uses: agenthunt/conventional-commit-checker-action@v2.0.0
with:
pr-title-regex: '^([^\s(]+)(?:\(([^)]+)\))?: (.+)'
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.88.2
uses: trufflesecurity/trufflehog@v3.88.14
with:
path: ./
base: ${{ github.event.repository.default_branch }}
+2
View File
@@ -39,6 +39,8 @@ jobs:
.backportrc.json
.env
docker-compose*
examples/**
.gitignore
- name: Install poetry
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -63,6 +63,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- 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@v3
with:
@@ -78,10 +84,13 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=${{ env.SHORT_SHA }}
# 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
@@ -90,9 +99,20 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
build-args: |
NEXT_PUBLIC_PROWLER_RELEASE_VERSION=v${{ env.RELEASE_TAG }}
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.RELEASE_TAG }}
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Trigger deployment
if: github.event_name == 'push'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
repository: ${{ secrets.CLOUD_DISPATCH }}
event-type: prowler-ui-deploy
client-payload: '{"sha": "${{ github.sha }}", "short_sha": "${{ env.SHORT_SHA }}"}'
+21
View File
@@ -6,6 +6,7 @@ on:
- "master"
- "v5.*"
paths:
- ".github/workflows/ui-pull-request.yml"
- "ui/**"
pull_request:
branches:
@@ -13,6 +14,9 @@ on:
- "v5.*"
paths:
- 'ui/**'
env:
UI_WORKING_DIR: ./ui
IMAGE_NAME: prowler-ui
jobs:
test-and-coverage:
@@ -39,3 +43,20 @@ jobs:
- name: Build the application
working-directory: ./ui
run: npm run build
test-container-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Container
uses: docker/build-push-action@v6
with:
context: ${{ env.UI_WORKING_DIR }}
# Always build using `prod` target
target: prod
push: false
tags: ${{ env.IMAGE_NAME }}:latest
outputs: type=docker
build-args: |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51LwpXXXX
+1 -1
View File
@@ -31,7 +31,7 @@ tags
*.DS_Store
# Prowler output
output/
/output
# Prowler found secrets
secrets-*/
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.12.8-alpine3.20
FROM python:3.12.9-alpine3.20
LABEL maintainer="https://github.com/prowler-cloud/prowler"
+24 -10
View File
@@ -71,10 +71,13 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe
| 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 | 561 | 81 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` |
| GCP | 77 | 13 -> `prowler gcp --list-services` | 4 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
| Azure | 139 | 18 -> `prowler azure --list-services` | 5 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 2 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
| AWS | 564 | 82 | 33 | 10 |
| GCP | 77 | 13 | 5 | 3 |
| Azure | 140 | 18 | 6 | 3 |
| Kubernetes | 83 | 7 | 2 | 7 |
| Microsoft365 | 5 | 2 | 1 | 0 |
> You can list the checks, services, compliance frameworks and categories with `prowler <provider> --list-checks`, `prowler <provider> --list-services`, `prowler <provider> --list-compliance` and `prowler <provider> --list-categories`.
# 💻 Installation
@@ -116,7 +119,7 @@ docker compose up -d
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
poetry shell
eval $(poetry env activate)
set -a
source .env
docker compose up postgres valkey -d
@@ -124,6 +127,11 @@ cd src/backend
python manage.py migrate --database admin
gunicorn -c config/guniconf.py config.wsgi:application
```
> [!IMPORTANT]
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
>
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
@@ -133,7 +141,7 @@ gunicorn -c config/guniconf.py config.wsgi:application
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
poetry shell
eval $(poetry env activate)
set -a
source .env
cd src/backend
@@ -146,7 +154,7 @@ python -m celery -A config.celery worker -l info -E
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
poetry shell
eval $(poetry env activate)
set -a
source .env
cd src/backend
@@ -167,7 +175,7 @@ npm start
## Prowler CLI
### Pip package
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python >= 3.9, < 3.13:
Prowler CLI is available as a project in [PyPI](https://pypi.org/project/prowler-cloud/), thus can be installed using pip with Python > 3.9.1, < 3.13:
```console
pip install prowler
@@ -197,15 +205,21 @@ The container images are available here:
### From GitHub
Python >= 3.9, < 3.13 is required with pip and poetry:
Python > 3.9.1, < 3.13 is required with pip and poetry:
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler
poetry shell
eval $(poetry env activate)
poetry install
python prowler.py -v
```
> [!IMPORTANT]
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
>
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
> If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths.
# 📐✏️ High level architecture
+14 -4
View File
@@ -23,10 +23,7 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
DJANGO_DB_CONNECTION_POOL_MIN_SIZE=4
DJANGO_DB_CONNECTION_POOL_MAX_SIZE=10
DJANGO_DB_CONNECTION_POOL_MAX_IDLE=36000
DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME=86400
DJANGO_SENTRY_DSN=
# PostgreSQL settings
# If running django and celery on host, use 'localhost', else use 'postgres-db'
@@ -43,3 +40,16 @@ POSTGRES_DB=prowler_db
VALKEY_HOST=[localhost|valkey]
VALKEY_PORT=6379
VALKEY_DB=0
# Sentry settings
SENTRY_ENVIRONMENT=local
SENTRY_RELEASE=local
# Social login credentials
DJANGO_GOOGLE_OAUTH_CLIENT_ID=""
DJANGO_GOOGLE_OAUTH_CLIENT_SECRET=""
DJANGO_GOOGLE_OAUTH_CALLBACK_URL=""
DJANGO_GITHUB_OAUTH_CLIENT_ID=""
DJANGO_GITHUB_OAUTH_CLIENT_SECRET=""
DJANGO_GITHUB_OAUTH_CALLBACK_URL=""
+27
View File
@@ -0,0 +1,27 @@
# Prowler API Changelog
All notable changes to the **Prowler API** are documented in this file.
---
## [v1.5.0] (Prowler v5.4.0)
### Added
- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906)
- Add API scan report system, now all scans launched from the API will generate a compressed file with the report in OCSF, CSV and HTML formats [(#6878)](https://github.com/prowler-cloud/prowler/pull/6878).
- Configurable Sentry integration [(#6874)](https://github.com/prowler-cloud/prowler/pull/6874)
### Changed
- Optimized `GET /findings` endpoint to improve response time and size [(#7019)](https://github.com/prowler-cloud/prowler/pull/7019).
---
## [v1.4.0] (Prowler v5.3.0)
### Changed
- Daily scheduled scan instances are now created beforehand with `SCHEDULED` state [(#6700)](https://github.com/prowler-cloud/prowler/pull/6700).
- Findings endpoints now require at least one date filter [(#6800)](https://github.com/prowler-cloud/prowler/pull/6800).
- Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863).
- Increase the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869).
---
+63
View File
@@ -269,3 +269,66 @@ poetry shell
cd src/backend
pytest
```
# Custom commands
Django provides a way to create custom commands that can be run from the command line.
> These commands can be found in: ```prowler/api/src/backend/api/management/commands```
To run a custom command, you need to be in the `prowler/api/src/backend` directory and run:
```console
poetry shell
python manage.py <command_name>
```
## Generate dummy data
```console
python manage.py findings --tenant
<TENANT_ID> --findings <NUM_FINDINGS> --re
sources <NUM_RESOURCES> --batch <TRANSACTION_BATCH_SIZE> --alias <ALIAS>
```
This command creates, for a given tenant, a provider, scan and a set of findings and resources related altogether.
> Scan progress and state are updated in real time.
> - 0-33%: Create resources.
> - 33-66%: Create findings.
> - 66%: Create resource-finding mapping.
>
> The last step is required to access the findings details, since the UI needs that to print all the information.
### Example
```console
~/backend $ poetry run python manage.py findings --tenant
fffb1893-3fc7-4623-a5d9-fae47da1c528 --findings 25000 --re
sources 1000 --batch 5000 --alias test-script
Starting data population
Tenant: fffb1893-3fc7-4623-a5d9-fae47da1c528
Alias: test-script
Resources: 1000
Findings: 25000
Batch size: 5000
Creating resources...
100%|███████████████████████| 1/1 [00:00<00:00, 7.72it/s]
Resources created successfully.
Creating findings...
100%|███████████████████████| 5/5 [00:05<00:00, 1.09s/it]
Findings created successfully.
Creating resource-finding mappings...
100%|███████████████████████| 5/5 [00:02<00:00, 1.81it/s]
Resource-finding mappings created successfully.
Successfully populated test data.
```
+1 -1
View File
@@ -28,7 +28,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 -E
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans,scan-reports,deletion -E --max-tasks-per-child 1
}
start_worker_beat() {
+892 -809
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -8,10 +8,11 @@ description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
name = "prowler-api"
package-mode = false
version = "1.3.0"
version = "1.5.0"
[tool.poetry.dependencies]
celery = {extras = ["pytest"], version = "^5.4.0"}
dj-rest-auth = {extras = ["with_social", "jwt"], version = "7.0.1"}
django = "5.1.5"
django-celery-beat = "^2.7.0"
django-celery-results = "^2.5.1"
@@ -27,16 +28,18 @@ drf-nested-routers = "^0.94.1"
drf-spectacular = "0.27.2"
drf-spectacular-jsonapi = "0.5.1"
gunicorn = "23.0.0"
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "v5.2"}
psycopg = {extras = ["pool", "binary"], version = "3.2.3"}
prowler = {git = "https://github.com/prowler-cloud/prowler.git", branch = "v5.4"}
psycopg2-binary = "2.9.9"
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
# Needed for prowler compatibility
python = ">=3.11,<3.13"
sentry-sdk = {extras = ["django"], version = "^2.20.0"}
uuid6 = "2024.7.10"
[tool.poetry.group.dev.dependencies]
bandit = "1.7.9"
coverage = "7.5.4"
django-silk = "5.3.2"
docker = "7.1.0"
freezegun = "1.5.1"
mypy = "1.10.1"
@@ -49,6 +52,7 @@ pytest-randomly = "3.15.0"
pytest-xdist = "3.6.1"
ruff = "0.5.0"
safety = "3.2.9"
tqdm = "4.67.1"
vulture = "2.14"
[tool.poetry.scripts]
+57
View File
@@ -0,0 +1,57 @@
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.db import transaction
from api.db_router import MainRouter
from api.db_utils import rls_transaction
from api.models import Membership, Role, Tenant, User, UserRoleRelationship
class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter):
@staticmethod
def get_user_by_email(email: str):
try:
return User.objects.get(email=email)
except User.DoesNotExist:
return None
def pre_social_login(self, request, sociallogin):
# Link existing accounts with the same email address
email = sociallogin.account.extra_data.get("email")
if email:
existing_user = self.get_user_by_email(email)
if existing_user:
sociallogin.connect(request, existing_user)
def save_user(self, request, sociallogin, form=None):
"""
Called after the user data is fully populated from the provider
and is about to be saved to the DB for the first time.
"""
with transaction.atomic(using=MainRouter.admin_db):
user = super().save_user(request, sociallogin, form)
user.save(using=MainRouter.admin_db)
tenant = Tenant.objects.using(MainRouter.admin_db).create(
name=f"{user.email.split('@')[0]} default tenant"
)
with rls_transaction(str(tenant.id)):
Membership.objects.using(MainRouter.admin_db).create(
user=user, tenant=tenant, role=Membership.RoleChoices.OWNER
)
role = Role.objects.using(MainRouter.admin_db).create(
name="admin",
tenant_id=tenant.id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)
UserRoleRelationship.objects.using(MainRouter.admin_db).create(
user=user,
role=role,
tenant_id=tenant.id,
)
return user
+13 -2
View File
@@ -1,18 +1,29 @@
ALLOWED_APPS = ("django", "socialaccount", "account", "authtoken", "silk")
class MainRouter:
default_db = "default"
admin_db = "admin"
def db_for_read(self, model, **hints): # noqa: F841
model_table_name = model._meta.db_table
if model_table_name.startswith("django_"):
if model_table_name.startswith("django_") or any(
model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS
):
return self.admin_db
return None
def db_for_write(self, model, **hints): # noqa: F841
model_table_name = model._meta.db_table
if model_table_name.startswith("django_"):
if any(model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS):
return self.admin_db
return None
def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: F841
return db == self.admin_db
def allow_relation(self, obj1, obj2, **hints): # noqa: F841
# Allow relations if both objects are in either "default" or "admin" db connectors
if {obj1._state.db, obj2._state.db} <= {self.default_db, self.admin_db}:
return True
return None
+31 -30
View File
@@ -6,10 +6,8 @@ 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 psycopg import connect as psycopg_connect
from psycopg.adapt import Dumper
from psycopg.types import TypeInfo
from psycopg.types.string import TextLoader
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
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
@@ -22,7 +20,6 @@ DB_PROWLER_USER = (
DB_PROWLER_PASSWORD = (
settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test"
)
TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
POSTGRES_TENANT_VAR = "api.tenant_id"
POSTGRES_USER_VAR = "api.user_id"
@@ -32,25 +29,21 @@ SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
@contextmanager
def psycopg_connection(database_alias: str):
"""
Context manager returning a psycopg 3 connection
for the specified 'database_alias' in Django settings.
"""
pg_conn = None
psycopg2_connection = None
try:
admin_db = settings.DATABASES[database_alias]
pg_conn = psycopg_connect(
psycopg2_connection = psycopg2_connect(
dbname=admin_db["NAME"],
user=admin_db["USER"],
password=admin_db["PASSWORD"],
host=admin_db["HOST"],
port=admin_db["PORT"],
)
yield pg_conn
yield psycopg2_connection
finally:
if pg_conn is not None:
pg_conn.close()
if psycopg2_connection is not None:
psycopg2_connection.close()
@contextmanager
@@ -66,7 +59,7 @@ def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
with transaction.atomic():
with connection.cursor() as cursor:
try:
# Just in case the value is a UUID object
# just in case the value is an UUID object
uuid.UUID(str(value))
except ValueError:
raise ValidationError("Must be a valid UUID")
@@ -194,24 +187,32 @@ class EnumType:
return self.value
def register_enum(apps, schema_editor, enum_class):
"""
psycopg 3 approach: register a loader + dumper for the given enum_class,
so we can read/write the custom Postgres ENUM seamlessly.
"""
with psycopg_connection(schema_editor.connection.alias) as conn:
ti = TypeInfo.fetch(conn, enum_class.enum_type_name)
def enum_adapter(enum_obj):
return AsIs(f"'{enum_obj.value}'::{enum_obj.__class__.enum_type_name}")
class EnumLoader(TextLoader):
def load(self, data):
return data
class EnumDumper(Dumper):
def dump(self, obj):
return f"'{obj.value}'::{obj.__class__.enum_type_name}"
def get_enum_oid(connection, enum_type_name: str):
with connection.cursor() as cursor:
cursor.execute("SELECT oid FROM pg_type WHERE typname = %s;", (enum_type_name,))
result = cursor.fetchone()
if result is None:
raise ValueError(f"Enum type '{enum_type_name}' not found")
return result[0]
conn.adapters.register_loader(ti.oid, EnumLoader)
conn.adapters.register_dumper(enum_class, EnumDumper)
def register_enum(apps, schema_editor, enum_class): # noqa: F841
with psycopg_connection(schema_editor.connection.alias) as connection:
enum_oid = get_enum_oid(connection, enum_class.enum_type_name)
enum_instance = new_type(
(enum_oid,),
enum_class.enum_type_name,
lambda value, cur: value, # noqa: F841
)
register_type(enum_instance, connection)
register_adapter(enum_class, enum_adapter)
# Postgres enum definition for member role
class MemberRoleEnum(EnumType):
+25 -16
View File
@@ -7,7 +7,7 @@ from rest_framework_json_api.serializers import ValidationError
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
def set_tenant(func):
def set_tenant(func=None, *, keep_tenant=False):
"""
Decorator to set the tenant context for a Celery task based on the provided tenant_id.
@@ -40,20 +40,29 @@ def set_tenant(func):
# The tenant context will be set before the task logic executes.
"""
@wraps(func)
@transaction.atomic
def wrapper(*args, **kwargs):
try:
tenant_id = kwargs.pop("tenant_id")
except KeyError:
raise KeyError("This task requires the tenant_id")
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
def decorator(func):
@wraps(func)
@transaction.atomic
def wrapper(*args, **kwargs):
try:
if not keep_tenant:
tenant_id = kwargs.pop("tenant_id")
else:
tenant_id = kwargs["tenant_id"]
except KeyError:
raise KeyError("This task requires the tenant_id")
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
return func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
return wrapper
if func is None:
return decorator
else:
return decorator(func)
+88 -42
View File
@@ -1,4 +1,4 @@
from datetime import date, datetime, timezone
from datetime import date, datetime, timedelta, timezone
from django.conf import settings
from django.db.models import Q
@@ -319,34 +319,41 @@ class FindingFilter(FilterSet):
field_name="resources__type", lookup_expr="icontains"
)
resource_tag_key = CharFilter(field_name="resources__tags__key")
resource_tag_key__in = CharInFilter(
field_name="resources__tags__key", lookup_expr="in"
)
resource_tag_key__icontains = CharFilter(
field_name="resources__tags__key", lookup_expr="icontains"
)
resource_tag_value = CharFilter(field_name="resources__tags__value")
resource_tag_value__in = CharInFilter(
field_name="resources__tags__value", lookup_expr="in"
)
resource_tag_value__icontains = CharFilter(
field_name="resources__tags__value", lookup_expr="icontains"
)
resource_tags = CharInFilter(
method="filter_resource_tag",
lookup_expr="in",
help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be "
"separated by commas.",
)
# Temporarily disabled until we implement tag filtering in the UI
# resource_tag_key = CharFilter(field_name="resources__tags__key")
# resource_tag_key__in = CharInFilter(
# field_name="resources__tags__key", lookup_expr="in"
# )
# resource_tag_key__icontains = CharFilter(
# field_name="resources__tags__key", lookup_expr="icontains"
# )
# resource_tag_value = CharFilter(field_name="resources__tags__value")
# resource_tag_value__in = CharInFilter(
# field_name="resources__tags__value", lookup_expr="in"
# )
# resource_tag_value__icontains = CharFilter(
# field_name="resources__tags__value", lookup_expr="icontains"
# )
# resource_tags = CharInFilter(
# method="filter_resource_tag",
# lookup_expr="in",
# help_text="Filter by resource tags `key:value` pairs.\nMultiple values may be "
# "separated by commas.",
# )
scan = UUIDFilter(method="filter_scan_id")
scan__in = UUIDInFilter(method="filter_scan_id_in")
inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date")
inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date")
inserted_at__gte = DateFilter(method="filter_inserted_at_gte")
inserted_at__lte = DateFilter(method="filter_inserted_at_lte")
inserted_at__gte = DateFilter(
method="filter_inserted_at_gte",
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
)
inserted_at__lte = DateFilter(
method="filter_inserted_at_lte",
help_text=f"Maximum date range is {settings.FINDINGS_MAX_DAYS_IN_RANGE} days.",
)
class Meta:
model = Finding
@@ -374,11 +381,51 @@ class FindingFilter(FilterSet):
},
}
@property
def qs(self):
# Force distinct results to prevent duplicates with many-to-many relationships
parent_qs = super().qs
return parent_qs.distinct()
def filter_queryset(self, queryset):
if not (self.data.get("scan") or self.data.get("scan__in")) and not (
self.data.get("inserted_at")
or self.data.get("inserted_at__date")
or self.data.get("inserted_at__gte")
or self.data.get("inserted_at__lte")
):
raise ValidationError(
[
{
"detail": "At least one date filter is required: filter[inserted_at], filter[inserted_at.gte], "
"or filter[inserted_at.lte].",
"status": 400,
"source": {"pointer": "/data/attributes/inserted_at"},
"code": "required",
}
]
)
gte_date = (
datetime.strptime(self.data.get("inserted_at__gte"), "%Y-%m-%d").date()
if self.data.get("inserted_at__gte")
else datetime.now(timezone.utc).date()
)
lte_date = (
datetime.strptime(self.data.get("inserted_at__lte"), "%Y-%m-%d").date()
if self.data.get("inserted_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/inserted_at"},
"code": "invalid",
}
]
)
return super().filter_queryset(queryset)
# Convert filter values to UUIDv7 values for use with partitioning
def filter_scan_id(self, queryset, name, value):
@@ -400,9 +447,7 @@ class FindingFilter(FilterSet):
)
return (
queryset.filter(id__gte=start)
.filter(id__lt=end)
.filter(scan__id=value_uuid)
queryset.filter(id__gte=start).filter(id__lt=end).filter(scan_id=value_uuid)
)
def filter_scan_id_in(self, queryset, name, value):
@@ -427,31 +472,32 @@ class FindingFilter(FilterSet):
]
)
if start == end:
return queryset.filter(id__gte=start).filter(scan__id__in=uuid_list)
return queryset.filter(id__gte=start).filter(scan_id__in=uuid_list)
else:
return (
queryset.filter(id__gte=start)
.filter(id__lt=end)
.filter(scan__id__in=uuid_list)
.filter(scan_id__in=uuid_list)
)
def filter_inserted_at(self, queryset, name, value):
value = self.maybe_date_to_datetime(value)
start = uuid7_start(datetime_to_uuid7(value))
datetime_value = self.maybe_date_to_datetime(value)
start = uuid7_start(datetime_to_uuid7(datetime_value))
end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1)))
return queryset.filter(id__gte=start).filter(inserted_at__date=value)
return queryset.filter(id__gte=start, id__lt=end)
def filter_inserted_at_gte(self, queryset, name, value):
value = self.maybe_date_to_datetime(value)
start = uuid7_start(datetime_to_uuid7(value))
datetime_value = self.maybe_date_to_datetime(value)
start = uuid7_start(datetime_to_uuid7(datetime_value))
return queryset.filter(id__gte=start).filter(inserted_at__gte=value)
return queryset.filter(id__gte=start)
def filter_inserted_at_lte(self, queryset, name, value):
value = self.maybe_date_to_datetime(value)
end = uuid7_start(datetime_to_uuid7(value))
datetime_value = self.maybe_date_to_datetime(value)
end = uuid7_start(datetime_to_uuid7(datetime_value + timedelta(days=1)))
return queryset.filter(id__lte=end).filter(inserted_at__lte=value)
return queryset.filter(id__lt=end)
def filter_resource_tag(self, queryset, name, value):
overall_query = Q()
@@ -122,6 +122,22 @@
"scanner_args": {}
}
},
{
"model": "api.provider",
"pk": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:45:26.352Z",
"updated_at": "2024-10-18T11:16:23.533Z",
"provider": "kubernetes",
"uid": "gke_lucky-coast-419309_us-central1_autopilot-cluster-2",
"alias": "k8s_testing_2",
"connected": true,
"connection_last_checked_at": "2024-10-18T11:16:23.503Z",
"metadata": {},
"scanner_args": {}
}
},
{
"model": "api.providersecret",
"pk": "11491b47-75ae-4f71-ad8d-3e630a72182e",
@@ -11,9 +11,7 @@
"unique_resource_count": 1,
"duration": 5,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled"
]
"checks_to_execute": ["accessanalyzer_enabled"]
},
"inserted_at": "2024-09-01T17:25:27.050Z",
"started_at": "2024-09-01T17:25:27.050Z",
@@ -33,9 +31,7 @@
"unique_resource_count": 1,
"duration": 20,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled"
]
"checks_to_execute": ["accessanalyzer_enabled"]
},
"inserted_at": "2024-09-02T17:24:27.050Z",
"started_at": "2024-09-02T17:24:27.050Z",
@@ -55,9 +51,7 @@
"unique_resource_count": 10,
"duration": 10,
"scanner_args": {
"checks_to_execute": [
"cloudsql_instance_automated_backups"
]
"checks_to_execute": ["cloudsql_instance_automated_backups"]
},
"inserted_at": "2024-09-02T19:26:27.050Z",
"started_at": "2024-09-02T19:26:27.050Z",
@@ -77,9 +71,7 @@
"unique_resource_count": 1,
"duration": 35,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled"
]
"checks_to_execute": ["accessanalyzer_enabled"]
},
"inserted_at": "2024-09-02T19:27:27.050Z",
"started_at": "2024-09-02T19:27:27.050Z",
@@ -97,9 +89,7 @@
"name": "test scheduled aws scan",
"state": "available",
"scanner_args": {
"checks_to_execute": [
"cloudformation_stack_outputs_find_secrets"
]
"checks_to_execute": ["cloudformation_stack_outputs_find_secrets"]
},
"scheduled_at": "2030-09-02T19:20:27.050Z",
"inserted_at": "2024-09-02T19:24:27.050Z",
@@ -178,9 +168,7 @@
"unique_resource_count": 19,
"progress": 100,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled"
]
"checks_to_execute": ["accessanalyzer_enabled"]
},
"duration": 7,
"scheduled_at": null,
@@ -190,6 +178,56 @@
"completed_at": "2024-10-18T10:46:05.127Z"
}
},
{
"model": "api.scan",
"pk": "6dd8925f-a52d-48de-a546-d2d90db30ab1",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "real scan azure",
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
"trigger": "manual",
"state": "completed",
"unique_resource_count": 20,
"progress": 100,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled",
"account_security_contact_information_is_registered"
]
},
"duration": 4,
"scheduled_at": null,
"inserted_at": "2024-10-18T11:16:21.358Z",
"updated_at": "2024-10-18T11:16:26.060Z",
"started_at": "2024-10-18T11:16:21.593Z",
"completed_at": "2024-10-18T11:16:26.060Z"
}
},
{
"model": "api.scan",
"pk": "4ca7ce89-3236-41a8-a369-8937bc152af5",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "real scan k8s",
"provider": "7791914f-d646-4fe2-b2ed-73f2c6499a36",
"trigger": "manual",
"state": "completed",
"unique_resource_count": 20,
"progress": 100,
"scanner_args": {
"checks_to_execute": [
"accessanalyzer_enabled",
"account_security_contact_information_is_registered"
]
},
"duration": 4,
"scheduled_at": null,
"inserted_at": "2024-10-18T11:16:21.358Z",
"updated_at": "2024-10-18T11:16:26.060Z",
"started_at": "2024-10-18T11:16:21.593Z",
"completed_at": "2024-10-18T11:16:26.060Z"
}
},
{
"model": "api.scan",
"pk": "01929f57-c0ee-7553-be0b-cbde006fb6f7",
@@ -0,0 +1,237 @@
import random
from datetime import datetime, timezone
from math import ceil
from uuid import uuid4
from django.core.management.base import BaseCommand
from tqdm import tqdm
from api.db_utils import rls_transaction
from api.models import (
Finding,
Provider,
Resource,
ResourceFindingMapping,
Scan,
StatusChoices,
)
from prowler.lib.check.models import CheckMetadata
class Command(BaseCommand):
help = "Populates the database with test data for performance testing."
def add_arguments(self, parser):
parser.add_argument(
"--tenant",
type=str,
required=True,
help="Tenant id for which the data will be populated.",
)
parser.add_argument(
"--resources",
type=int,
required=True,
help="The number of resources to create.",
)
parser.add_argument(
"--findings",
type=int,
required=True,
help="The number of findings to create.",
)
parser.add_argument(
"--batch", type=int, required=True, help="The batch size for bulk creation."
)
parser.add_argument(
"--alias",
type=str,
required=False,
help="Optional alias for the provider and scan",
)
def handle(self, *args, **options):
tenant_id = options["tenant"]
num_resources = options["resources"]
num_findings = options["findings"]
batch_size = options["batch"]
alias = options["alias"] or "Testing"
uid_token = str(uuid4())
self.stdout.write(self.style.NOTICE("Starting data population"))
self.stdout.write(self.style.NOTICE(f"\tTenant: {tenant_id}"))
self.stdout.write(self.style.NOTICE(f"\tAlias: {alias}"))
self.stdout.write(self.style.NOTICE(f"\tResources: {num_resources}"))
self.stdout.write(self.style.NOTICE(f"\tFindings: {num_findings}"))
self.stdout.write(self.style.NOTICE(f"\tBatch size: {batch_size}\n\n"))
# Resource metadata
possible_regions = [
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"sa-east-1",
]
possible_services = []
possible_types = []
bulk_check_metadata = CheckMetadata.get_bulk(provider="aws")
for check_metadata in bulk_check_metadata.values():
if check_metadata.ServiceName not in possible_services:
possible_services.append(check_metadata.ServiceName)
if (
check_metadata.ResourceType
and check_metadata.ResourceType not in possible_types
):
possible_types.append(check_metadata.ResourceType)
with rls_transaction(tenant_id):
provider, _ = Provider.all_objects.get_or_create(
tenant_id=tenant_id,
provider="aws",
connected=True,
uid=str(random.randint(100000000000, 999999999999)),
defaults={
"alias": alias,
},
)
with rls_transaction(tenant_id):
scan = Scan.all_objects.create(
tenant_id=tenant_id,
provider=provider,
name=alias,
trigger="manual",
state="executing",
progress=0,
started_at=datetime.now(timezone.utc),
)
scan_state = "completed"
try:
# Create resources
resources = []
for i in range(num_resources):
resources.append(
Resource(
tenant_id=tenant_id,
provider_id=provider.id,
uid=f"testing-{uid_token}-{i}",
name=f"Testing {uid_token}-{i}",
region=random.choice(possible_regions),
service=random.choice(possible_services),
type=random.choice(possible_types),
)
)
num_batches = ceil(len(resources) / batch_size)
self.stdout.write(self.style.WARNING("Creating resources..."))
for i in tqdm(range(0, len(resources), batch_size), total=num_batches):
with rls_transaction(tenant_id):
Resource.all_objects.bulk_create(resources[i : i + batch_size])
self.stdout.write(self.style.SUCCESS("Resources created successfully.\n\n"))
with rls_transaction(tenant_id):
scan.progress = 33
scan.save()
# Create Findings
findings = []
possible_deltas = ["new", "changed", None]
possible_severities = ["critical", "high", "medium", "low"]
findings_resources_mapping = []
for i in range(num_findings):
severity = random.choice(possible_severities)
check_id = random.randint(1, 1000)
assigned_resource_num = random.randint(0, len(resources) - 1)
assigned_resource = resources[assigned_resource_num]
findings_resources_mapping.append(assigned_resource_num)
findings.append(
Finding(
tenant_id=tenant_id,
scan=scan,
uid=f"testing-{uid_token}-{i}",
delta=random.choice(possible_deltas),
check_id=f"check-{check_id}",
status=random.choice(list(StatusChoices)),
severity=severity,
impact=severity,
raw_result={},
check_metadata={
"checktitle": f"Test title for check {check_id}",
"risk": f"Testing risk {uid_token}-{i}",
"provider": "aws",
"severity": severity,
"categories": ["category1", "category2", "category3"],
"description": "This is a random description that should not matter for testing purposes.",
"servicename": assigned_resource.service,
"resourcetype": assigned_resource.type,
},
)
)
num_batches = ceil(len(findings) / batch_size)
self.stdout.write(self.style.WARNING("Creating findings..."))
for i in tqdm(range(0, len(findings), batch_size), total=num_batches):
with rls_transaction(tenant_id):
Finding.all_objects.bulk_create(findings[i : i + batch_size])
self.stdout.write(self.style.SUCCESS("Findings created successfully.\n\n"))
with rls_transaction(tenant_id):
scan.progress = 66
scan.save()
# Create ResourceFindingMapping
mappings = []
for index, f in enumerate(findings):
mappings.append(
ResourceFindingMapping(
tenant_id=tenant_id,
resource=resources[findings_resources_mapping[index]],
finding=f,
)
)
num_batches = ceil(len(mappings) / batch_size)
self.stdout.write(
self.style.WARNING("Creating resource-finding mappings...")
)
for i in tqdm(range(0, len(mappings), batch_size), total=num_batches):
with rls_transaction(tenant_id):
ResourceFindingMapping.objects.bulk_create(
mappings[i : i + batch_size]
)
self.stdout.write(
self.style.SUCCESS(
"Resource-finding mappings created successfully.\n\n"
)
)
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to populate test data: {e}"))
scan_state = "failed"
finally:
scan.completed_at = datetime.now(timezone.utc)
scan.duration = int(
(datetime.now(timezone.utc) - scan.started_at).total_seconds()
)
scan.progress = 100
scan.state = scan_state
scan.unique_resource_count = num_resources
with rls_transaction(tenant_id):
scan.save()
self.stdout.write(self.style.NOTICE("Successfully populated test data."))
@@ -0,0 +1,25 @@
# Generated by Django 5.1.5 on 2025-01-28 15:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0006_findings_first_seen"),
]
operations = [
migrations.AddIndex(
model_name="scan",
index=models.Index(
fields=["tenant_id", "provider_id", "state", "inserted_at"],
name="scans_prov_state_insert_idx",
),
),
migrations.AddIndex(
model_name="scansummary",
index=models.Index(
fields=["tenant_id", "scan_id"], name="scan_summaries_tenant_scan_idx"
),
),
]
@@ -0,0 +1,64 @@
import json
from datetime import datetime, timedelta, timezone
import django.db.models.deletion
from django.db import migrations, models
from django_celery_beat.models import PeriodicTask
from api.db_utils import rls_transaction
from api.models import Scan, StateChoices
def migrate_daily_scheduled_scan_tasks(apps, schema_editor):
for daily_scheduled_scan_task in PeriodicTask.objects.filter(
task="scan-perform-scheduled"
):
task_kwargs = json.loads(daily_scheduled_scan_task.kwargs)
tenant_id = task_kwargs["tenant_id"]
provider_id = task_kwargs["provider_id"]
current_time = datetime.now(timezone.utc)
scheduled_time_today = datetime.combine(
current_time.date(),
daily_scheduled_scan_task.start_time.time(),
tzinfo=timezone.utc,
)
if current_time < scheduled_time_today:
next_scan_date = scheduled_time_today
else:
next_scan_date = scheduled_time_today + timedelta(days=1)
with rls_transaction(tenant_id):
Scan.objects.create(
tenant_id=tenant_id,
name="Daily scheduled scan",
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
scheduled_at=next_scan_date,
scheduler_task_id=daily_scheduled_scan_task.id,
)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0007_scan_and_scan_summaries_indexes"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
migrations.AddField(
model_name="scan",
name="scheduler_task",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="django_celery_beat.periodictask",
),
),
migrations.RunPython(migrate_daily_scheduled_scan_tasks),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.5 on 2025-02-07 09:42
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0008_daily_scheduled_tasks_update"),
]
operations = [
migrations.AlterField(
model_name="provider",
name="uid",
field=models.CharField(
max_length=250,
validators=[django.core.validators.MinLengthValidator(3)],
verbose_name="Unique identifier for the provider, set by the provider",
),
),
]
@@ -0,0 +1,109 @@
from functools import partial
from django.db import connection, migrations
def create_index_on_partitions(
apps, schema_editor, parent_table: str, index_name: str, index_details: str
):
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT inhrelid::regclass::text
FROM pg_inherits
WHERE inhparent = %s::regclass;
""",
[parent_table],
)
partitions = [row[0] for row in cursor.fetchall()]
# Iterate over partitions and create index concurrently.
# Note: PostgreSQL does not allow CONCURRENTLY inside a transaction,
# so we need atomic = False for this migration.
for partition in partitions:
sql = (
f"CREATE INDEX CONCURRENTLY IF NOT EXISTS {partition.replace('.', '_')}_{index_name} ON {partition} "
f"{index_details};"
)
schema_editor.execute(sql)
def drop_index_on_partitions(apps, schema_editor, parent_table: str, index_name: str):
with schema_editor.connection.cursor() as cursor:
cursor.execute(
"""
SELECT inhrelid::regclass::text
FROM pg_inherits
WHERE inhparent = %s::regclass;
""",
[parent_table],
)
partitions = [row[0] for row in cursor.fetchall()]
# Iterate over partitions and drop index concurrently.
for partition in partitions:
partition_index = f"{partition.replace('.', '_')}_{index_name}"
sql = f"DROP INDEX CONCURRENTLY IF EXISTS {partition_index};"
schema_editor.execute(sql)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("api", "0009_increase_provider_uid_maximum_length"),
]
operations = [
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="findings",
index_name="findings_tenant_and_id_idx",
index_details="(tenant_id, id)",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="findings",
index_name="findings_tenant_and_id_idx",
),
),
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="findings",
index_name="find_tenant_scan_idx",
index_details="(tenant_id, scan_id)",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="findings",
index_name="find_tenant_scan_idx",
),
),
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="findings",
index_name="find_tenant_scan_id_idx",
index_details="(tenant_id, scan_id, id)",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="findings",
index_name="find_tenant_scan_id_idx",
),
),
migrations.RunPython(
partial(
create_index_on_partitions,
parent_table="findings",
index_name="find_delta_new_idx",
index_details="(tenant_id, id) where delta = 'new'",
),
reverse_code=partial(
drop_index_on_partitions,
parent_table="findings",
index_name="find_delta_new_idx",
),
),
]
@@ -0,0 +1,49 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0010_findings_performance_indexes_partitions"),
]
operations = [
migrations.AddIndex(
model_name="finding",
index=models.Index(
fields=["tenant_id", "id"], name="findings_tenant_and_id_idx"
),
),
migrations.AddIndex(
model_name="finding",
index=models.Index(
fields=["tenant_id", "scan_id"], name="find_tenant_scan_idx"
),
),
migrations.AddIndex(
model_name="finding",
index=models.Index(
fields=["tenant_id", "scan_id", "id"], name="find_tenant_scan_id_idx"
),
),
migrations.AddIndex(
model_name="finding",
index=models.Index(
condition=models.Q(("delta", "new")),
fields=["tenant_id", "id"],
name="find_delta_new_idx",
),
),
migrations.AddIndex(
model_name="resourcetagmapping",
index=models.Index(
fields=["tenant_id", "resource_id"], name="resource_tag_tenant_idx"
),
),
migrations.AddIndex(
model_name="resource",
index=models.Index(
fields=["tenant_id", "service", "region", "type"],
name="resource_tenant_metadata_idx",
),
),
]
@@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0011_findings_performance_indexes_parent"),
]
operations = [
migrations.AddField(
model_name="scan",
name="output_location",
field=models.CharField(blank=True, max_length=200, null=True),
),
]
+38 -3
View File
@@ -11,6 +11,7 @@ from django.core.validators import MinLengthValidator
from django.db import models
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 psqlextra.manager import PostgresManager
from psqlextra.models import PostgresPartitionedModel
@@ -226,13 +227,13 @@ class Provider(RowLevelSecurityProtectedModel):
@staticmethod
def validate_kubernetes_uid(value):
if not re.match(
r"(^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$)|(^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$)",
r"^[a-z0-9][A-Za-z0-9_.:\/-]{1,250}$",
value,
):
raise ModelValidationError(
detail="The value must either be a valid Kubernetes UID (up to 63 characters, "
"starting and ending with a lowercase letter or number, containing only "
"lowercase alphanumeric characters and hyphens) or a valid EKS ARN.",
"lowercase alphanumeric characters and hyphens) or a valid AWS EKS Cluster ARN, GCP GKE Context Name or Azure AKS Cluster Name.",
code="kubernetes-uid",
pointer="/data/attributes/uid",
)
@@ -246,7 +247,7 @@ class Provider(RowLevelSecurityProtectedModel):
)
uid = models.CharField(
"Unique identifier for the provider, set by the provider",
max_length=63,
max_length=250,
blank=False,
validators=[MinLengthValidator(3)],
)
@@ -410,6 +411,10 @@ class Scan(RowLevelSecurityProtectedModel):
started_at = models.DateTimeField(null=True, blank=True)
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
)
output_location = models.CharField(blank=True, null=True, max_length=200)
# TODO: mutelist foreign key
class Meta(RowLevelSecurityProtectedModel.Meta):
@@ -428,6 +433,10 @@ class Scan(RowLevelSecurityProtectedModel):
fields=["provider", "state", "trigger", "scheduled_at"],
name="scans_prov_state_trig_sche_idx",
),
models.Index(
fields=["tenant_id", "provider_id", "state", "inserted_at"],
name="scans_prov_state_insert_idx",
),
]
class JSONAPIMeta:
@@ -544,6 +553,10 @@ class Resource(RowLevelSecurityProtectedModel):
fields=["uid", "region", "service", "name"],
name="resource_uid_reg_serv_name_idx",
),
models.Index(
fields=["tenant_id", "service", "region", "type"],
name="resource_tenant_metadata_idx",
),
GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
]
@@ -591,6 +604,12 @@ class ResourceTagMapping(RowLevelSecurityProtectedModel):
),
]
indexes = [
models.Index(
fields=["tenant_id", "resource_id"], name="resource_tag_tenant_idx"
),
]
class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
"""
@@ -689,7 +708,17 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
],
name="findings_filter_idx",
),
models.Index(fields=["tenant_id", "id"], name="findings_tenant_and_id_idx"),
GinIndex(fields=["text_search"], name="gin_findings_search_idx"),
models.Index(fields=["tenant_id", "scan_id"], name="find_tenant_scan_idx"),
models.Index(
fields=["tenant_id", "scan_id", "id"], name="find_tenant_scan_id_idx"
),
models.Index(
fields=["tenant_id", "id"],
condition=Q(delta="new"),
name="find_delta_new_idx",
),
]
class JSONAPIMeta:
@@ -1100,6 +1129,12 @@ class ScanSummary(RowLevelSecurityProtectedModel):
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
indexes = [
models.Index(
fields=["tenant_id", "scan_id"],
name="scan_summaries_tenant_scan_idx",
)
]
class JSONAPIMeta:
resource_name = "scan-summaries"
+158 -150
View File
@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.2.0
version: 1.5.0
description: |-
Prowler API specification.
@@ -346,6 +346,9 @@ paths:
schema:
type: string
format: date
description: At least one of the variations of the `filter[inserted_at]` filter
must be provided.
required: true
- in: query
name: filter[inserted_at__date]
schema:
@@ -356,11 +359,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -478,51 +483,6 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_key]
schema:
type: string
- in: query
name: filter[resource_tag_key__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_key__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_value]
schema:
type: string
- in: query
name: filter[resource_tag_value__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_value__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tags]
schema:
type: array
items:
type: string
description: |-
Filter by resource tags `key:value` pairs.
Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_type]
schema:
@@ -714,8 +674,6 @@ paths:
items:
type: string
enum:
- id
- -id
- status
- -status
- severity
@@ -908,11 +866,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -1030,51 +990,6 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_key]
schema:
type: string
- in: query
name: filter[resource_tag_key__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_key__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_value]
schema:
type: string
- in: query
name: filter[resource_tag_value__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_value__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tags]
schema:
type: array
items:
type: string
description: |-
Filter by resource tags `key:value` pairs.
Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_type]
schema:
@@ -1242,8 +1157,6 @@ paths:
items:
type: string
enum:
- id
- -id
- status
- -status
- severity
@@ -1284,7 +1197,6 @@ paths:
- services
- regions
- resource_types
- tags
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
@@ -1370,6 +1282,9 @@ paths:
schema:
type: string
format: date
description: At least one of the variations of the `filter[inserted_at]` filter
must be provided.
required: true
- in: query
name: filter[inserted_at__date]
schema:
@@ -1380,11 +1295,13 @@ paths:
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[inserted_at__lte]
schema:
type: string
format: date
description: Maximum date range is 7 days.
- in: query
name: filter[provider]
schema:
@@ -1502,51 +1419,6 @@ paths:
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_key]
schema:
type: string
- in: query
name: filter[resource_tag_key__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_key__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tag_value]
schema:
type: string
- in: query
name: filter[resource_tag_value__icontains]
schema:
type: string
- in: query
name: filter[resource_tag_value__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_tags]
schema:
type: array
items:
type: string
description: |-
Filter by resource tags `key:value` pairs.
Multiple values may be separated by commas.
explode: false
style: form
- in: query
name: filter[resource_type]
schema:
@@ -1714,8 +1586,6 @@ paths:
items:
type: string
enum:
- id
- -id
- status
- -status
- severity
@@ -4235,6 +4105,43 @@ paths:
schema:
$ref: '#/components/schemas/ScanUpdateResponse'
description: ''
/api/v1/scans/{id}/report:
get:
operationId: scans_report_retrieve
description: Returns a ZIP file containing the requested report
summary: Download ZIP report
parameters:
- in: query
name: fields[scan-reports]
schema:
type: array
items:
type: string
enum:
- id
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this scan.
required: true
tags:
- Scan
security:
- jwtAuth: []
responses:
'200':
description: Report obtained successfully
'202':
description: The task is in progress
'403':
description: There is a problem with credentials
'404':
description: The scan has no reports
/api/v1/schedules/daily:
post:
operationId: schedules_daily_create
@@ -5126,6 +5033,35 @@ paths:
schema:
$ref: '#/components/schemas/TokenRefreshResponse'
description: ''
/api/v1/tokens/switch:
post:
operationId: tokens_switch_create
description: Switch tenant by providing a valid tenant ID. The authenticated
user must belong to the tenant.
summary: Switch tenant using a valid tenant ID
tags:
- Token
requestBody:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TokenSwitchTenantRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/TokenSwitchTenantRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/TokenSwitchTenantRequest'
required: true
security:
- jwtAuth: []
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/TokenSwitchTenantResponse'
description: ''
/api/v1/users:
get:
operationId: users_list
@@ -6087,13 +6023,10 @@ components:
type: array
items:
type: string
tags:
description: Tags are described as key-value pairs.
required:
- services
- regions
- resource_types
- tags
FindingMetadataResponse:
type: object
properties:
@@ -6753,7 +6686,7 @@ components:
type: integer
fail:
type: integer
manual:
muted:
type: integer
total:
type: integer
@@ -7810,7 +7743,7 @@ components:
uid:
type: string
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
minLength: 3
alias:
type: string
@@ -7922,7 +7855,7 @@ components:
uid:
type: string
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
minLength: 3
required:
- uid
@@ -7966,7 +7899,7 @@ components:
type: string
minLength: 3
title: Unique identifier for the provider, set by the provider
maxLength: 63
maxLength: 250
required:
- uid
required:
@@ -10180,6 +10113,81 @@ components:
$ref: '#/components/schemas/Token'
required:
- data
TokenSwitchTenant:
type: object
required:
- type
additionalProperties: false
properties:
type:
allOf:
- $ref: '#/components/schemas/TokenSwitchTenantTypeEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
attributes:
type: object
properties:
tenant_id:
type: string
format: uuid
writeOnly: true
description: The tenant ID for which to request a new token.
access:
type: string
readOnly: true
refresh:
type: string
readOnly: true
required:
- tenant_id
TokenSwitchTenantRequest:
type: object
properties:
data:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- tokens-switch-tenant
attributes:
type: object
properties:
tenant_id:
type: string
format: uuid
writeOnly: true
description: The tenant ID for which to request a new token.
access:
type: string
readOnly: true
minLength: 1
refresh:
type: string
readOnly: true
minLength: 1
required:
- tenant_id
required:
- data
TokenSwitchTenantResponse:
type: object
properties:
data:
$ref: '#/components/schemas/TokenSwitchTenant'
required:
- data
TokenSwitchTenantTypeEnum:
type: string
enum:
- tokens-switch-tenant
TokenTypeEnum:
type: string
enum:
@@ -3,6 +3,8 @@ from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
from django.urls import reverse
from rest_framework.test import APIClient
from api.models import Membership, User
@pytest.mark.django_db
def test_basic_authentication():
@@ -177,3 +179,122 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi
user2_me = client.get(reverse("user-me"), headers=user2_headers)
assert user2_me.status_code == 200
assert user2_me.json()["data"]["attributes"]["email"] == user2_email
@pytest.mark.django_db
class TestTokenSwitchTenant:
def test_switch_tenant_with_valid_token(self, tenants_fixture, providers_fixture):
client = APIClient()
test_user = "test_email@prowler.com"
test_password = "test_password"
# Check that we can create a new user without any kind of authentication
user_creation_response = client.post(
reverse("user-list"),
data={
"data": {
"type": "users",
"attributes": {
"name": "test",
"email": test_user,
"password": test_password,
},
}
},
format="vnd.api+json",
)
assert user_creation_response.status_code == 201
# Create a new relationship between this user and another tenant
tenant_id = tenants_fixture[0].id
user_instance = User.objects.get(email=test_user)
Membership.objects.create(user=user_instance, tenant_id=tenant_id)
# Check that using our new user's credentials we can authenticate and get the providers
access_token, _ = get_api_tokens(client, test_user, test_password)
auth_headers = get_authorization_header(access_token)
user_me_response = client.get(
reverse("user-me"),
headers=auth_headers,
)
assert user_me_response.status_code == 200
# Assert this user belongs to two tenants
assert (
user_me_response.json()["data"]["relationships"]["memberships"]["meta"][
"count"
]
== 2
)
provider_response = client.get(
reverse("provider-list"),
headers=auth_headers,
)
assert provider_response.status_code == 200
# Empty response since there are no providers in this tenant
assert not provider_response.json()["data"]
switch_tenant_response = client.post(
reverse("token-switch"),
data={
"data": {
"type": "tokens-switch-tenant",
"attributes": {"tenant_id": tenant_id},
}
},
headers=auth_headers,
)
assert switch_tenant_response.status_code == 200
new_access_token = switch_tenant_response.json()["data"]["attributes"]["access"]
new_auth_headers = get_authorization_header(new_access_token)
provider_response = client.get(
reverse("provider-list"),
headers=new_auth_headers,
)
assert provider_response.status_code == 200
# Now it must be data because we switched to another tenant with providers
assert provider_response.json()["data"]
def test_switch_tenant_with_invalid_token(self, create_test_user, tenants_fixture):
client = APIClient()
access_token, refresh_token = get_api_tokens(
client, create_test_user.email, TEST_PASSWORD
)
auth_headers = get_authorization_header(access_token)
invalid_token_response = client.post(
reverse("token-switch"),
data={
"data": {
"type": "tokens-switch-tenant",
"attributes": {"tenant_id": "invalid_tenant_id"},
}
},
headers=auth_headers,
)
assert invalid_token_response.status_code == 400
assert invalid_token_response.json()["errors"][0]["code"] == "invalid"
assert (
invalid_token_response.json()["errors"][0]["detail"]
== "Must be a valid UUID."
)
invalid_tenant_response = client.post(
reverse("token-switch"),
data={
"data": {
"type": "tokens-switch-tenant",
"attributes": {"tenant_id": tenants_fixture[-1].id},
}
},
headers=auth_headers,
)
assert invalid_tenant_response.status_code == 400
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
"Tenant does not exist or user is not a " "member."
)
+4 -3
View File
@@ -274,9 +274,10 @@ class TestValidateInvitation:
expired_time = datetime.now(timezone.utc) - timedelta(days=1)
invitation.expires_at = expired_time
with patch("api.utils.Invitation.objects.using") as mock_using, patch(
"api.utils.datetime"
) as mock_datetime:
with (
patch("api.utils.Invitation.objects.using") as mock_using,
patch("api.utils.datetime") as mock_datetime,
):
mock_db = mock_using.return_value
mock_db.get.return_value = invitation
mock_datetime.now.return_value = datetime.now(timezone.utc)
+244 -42
View File
@@ -1,10 +1,15 @@
import glob
import io
import json
import os
from datetime import datetime, timedelta, timezone
from unittest.mock import ANY, Mock, patch
import jwt
import pytest
from botocore.exceptions import NoCredentialsError
from conftest import API_JSON_CONTENT_TYPE, TEST_PASSWORD, TEST_USER
from django.conf import settings
from django.urls import reverse
from rest_framework import status
@@ -19,6 +24,7 @@ from api.models import (
RoleProviderGroupRelationship,
Scan,
StateChoices,
Task,
User,
UserRoleRelationship,
)
@@ -27,6 +33,12 @@ from api.rls import Tenant
TODAY = str(datetime.today().date())
def today_after_n_days(n_days: int) -> str:
return datetime.strftime(
datetime.today().date() + timedelta(days=n_days), "%Y-%m-%d"
)
@pytest.mark.django_db
class TestUserViewSet:
def test_users_list(self, authenticated_client, create_test_user):
@@ -878,6 +890,16 @@ class TestProviderViewSet:
"uid": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "kubernetes",
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
"alias": "EKS",
},
{
"provider": "kubernetes",
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
"alias": "GKE",
},
{
"provider": "azure",
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
@@ -2062,9 +2084,9 @@ class TestScanViewSet:
("started_at.gte", "2024-01-01", 3),
("started_at.lte", "2024-01-01", 0),
("trigger", Scan.TriggerChoices.MANUAL, 1),
("state", StateChoices.AVAILABLE, 2),
("state", StateChoices.AVAILABLE, 1),
("state", StateChoices.FAILED, 1),
("state.in", f"{StateChoices.FAILED},{StateChoices.AVAILABLE}", 3),
("state.in", f"{StateChoices.FAILED},{StateChoices.AVAILABLE}", 2),
("trigger", Scan.TriggerChoices.MANUAL, 1),
]
),
@@ -2139,6 +2161,159 @@ class TestScanViewSet:
response = authenticated_client.get(reverse("scan-list"), {"sort": "invalid"})
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_report_executing(self, authenticated_client, scans_fixture):
"""
When the scan is still executing (state == EXECUTING), the view should return
the task data with HTTP 202 and a Content-Location header.
"""
scan = scans_fixture[0]
scan.state = StateChoices.EXECUTING
scan.save()
task = Task.objects.create(tenant_id=scan.tenant_id)
dummy_task_data = {"id": str(task.id), "state": StateChoices.EXECUTING}
scan.task = task
scan.save()
with patch(
"api.v1.views.TaskSerializer",
return_value=type("DummySerializer", (), {"data": dummy_task_data}),
):
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == status.HTTP_202_ACCEPTED
assert "Content-Location" in response
assert dummy_task_data["id"] in response["Content-Location"]
def test_report_celery_task_executing(self, authenticated_client, scans_fixture):
"""
When the scan is not executing but a related celery task exists and is running,
the view should return that task data with HTTP 202.
"""
scan = scans_fixture[0]
scan.state = StateChoices.COMPLETED
scan.output_location = "dummy"
scan.save()
dummy_task = Task.objects.create(tenant_id=scan.tenant_id)
dummy_task.id = "dummy-task-id"
dummy_task_data = {"id": dummy_task.id, "state": StateChoices.EXECUTING}
with patch("api.v1.views.Task.objects.get", return_value=dummy_task), patch(
"api.v1.views.TaskSerializer",
return_value=type("DummySerializer", (), {"data": dummy_task_data}),
):
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == status.HTTP_202_ACCEPTED
assert "Content-Location" in response
assert dummy_task_data["id"] in response["Content-Location"]
def test_report_no_output_location(self, authenticated_client, scans_fixture):
"""
If the scan does not have an output_location, the view should return a 404.
"""
scan = scans_fixture[0]
scan.state = StateChoices.COMPLETED
scan.output_location = ""
scan.save()
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["errors"]["detail"] == "The scan has no reports."
def test_report_s3_no_credentials(
self, authenticated_client, scans_fixture, monkeypatch
):
"""
When output_location is an S3 URL and get_s3_client() raises a credentials exception,
the view should return HTTP 403 with the proper error message.
"""
scan = scans_fixture[0]
bucket = "test-bucket"
key = "report.zip"
scan.output_location = f"s3://{bucket}/{key}"
scan.state = StateChoices.COMPLETED
scan.save()
def fake_get_s3_client():
raise NoCredentialsError()
monkeypatch.setattr("api.v1.views.get_s3_client", fake_get_s3_client)
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert (
response.json()["errors"]["detail"]
== "There is a problem with credentials."
)
def test_report_s3_success(self, authenticated_client, scans_fixture, monkeypatch):
"""
When output_location is an S3 URL and the S3 client returns the file successfully,
the view should return the ZIP file with HTTP 200 and proper headers.
"""
scan = scans_fixture[0]
bucket = "test-bucket"
key = "report.zip"
scan.output_location = f"s3://{bucket}/{key}"
scan.state = StateChoices.COMPLETED
scan.save()
monkeypatch.setattr(
"api.v1.views.env", type("env", (), {"str": lambda self, key: bucket})()
)
class FakeS3Client:
def get_object(self, Bucket, Key):
assert Bucket == bucket
assert Key == key
return {"Body": io.BytesIO(b"s3 zip content")}
monkeypatch.setattr("api.v1.views.get_s3_client", lambda: FakeS3Client())
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == 200
expected_filename = os.path.basename("report.zip")
content_disposition = response.get("Content-Disposition")
assert content_disposition.startswith('attachment; filename="')
assert f'filename="{expected_filename}"' in content_disposition
assert response.content == b"s3 zip content"
def test_report_local_file(
self, authenticated_client, scans_fixture, tmp_path, monkeypatch
):
"""
When output_location is a local file path, the view should read the file from disk
and return it with proper headers.
"""
scan = scans_fixture[0]
file_content = b"local zip file content"
file_path = tmp_path / "report.zip"
file_path.write_bytes(file_content)
scan.output_location = str(file_path)
scan.state = StateChoices.COMPLETED
scan.save()
monkeypatch.setattr(
glob,
"glob",
lambda pattern: [str(file_path)] if pattern == str(file_path) else [],
)
url = reverse("scan-report", kwargs={"pk": scan.id})
response = authenticated_client.get(url)
assert response.status_code == 200
assert response.content == file_content
content_disposition = response.get("Content-Disposition")
assert content_disposition.startswith('attachment; filename="')
assert f'filename="{file_path.name}"' in content_disposition
@pytest.mark.django_db
class TestTaskViewSet:
@@ -2379,12 +2554,33 @@ class TestResourceViewSet:
@pytest.mark.django_db
class TestFindingViewSet:
def test_findings_list_none(self, authenticated_client):
response = authenticated_client.get(reverse("finding-list"))
response = authenticated_client.get(
reverse("finding-list"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
def test_findings_list(self, authenticated_client, findings_fixture):
def test_findings_list_no_date_filter(self, authenticated_client):
response = authenticated_client.get(reverse("finding-list"))
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "required"
def test_findings_date_range_too_large(self, authenticated_client):
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[inserted_at.lte]": today_after_n_days(
-(settings.FINDINGS_MAX_DAYS_IN_RANGE + 1)
),
},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "invalid"
def test_findings_list(self, authenticated_client, findings_fixture):
response = authenticated_client.get(
reverse("finding-list"), {"filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(findings_fixture)
assert (
@@ -2397,14 +2593,15 @@ class TestFindingViewSet:
[
("resources", ["resources"]),
("scan", ["scans"]),
("resources.provider,scan", ["resources", "scans", "providers"]),
("resources,scan.provider", ["resources", "scans", "providers"]),
],
)
def test_findings_list_include(
self, include_values, expected_resources, authenticated_client, findings_fixture
):
response = authenticated_client.get(
reverse("finding-list"), {"include": include_values}
reverse("finding-list"),
{"include": include_values, "filter[inserted_at]": TODAY},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(findings_fixture)
@@ -2439,30 +2636,31 @@ class TestFindingViewSet:
("service.icontains", "ec", 1),
("inserted_at", "2024-01-01", 0),
("inserted_at.date", "2024-01-01", 0),
("inserted_at.gte", "2024-01-01", 2),
("inserted_at.gte", today_after_n_days(-1), 2),
(
"inserted_at.lte",
"2028-12-31",
today_after_n_days(1),
2,
), # TODO: To avoid having to modify this value and to ensure that the tests always work, we should set the time before the fixtures are inserted
("updated_at.lte", "2024-01-01", 0),
),
("updated_at.lte", today_after_n_days(-1), 0),
("resource_type.icontains", "prowler", 2),
# full text search on finding
("search", "dev-qa", 1),
("search", "orange juice", 1),
# full text search on resource
("search", "ec2", 2),
# full text search on finding tags
("search", "value2", 2),
("resource_tag_key", "key", 2),
("resource_tag_key__in", "key,key2", 2),
("resource_tag_key__icontains", "key", 2),
("resource_tag_value", "value", 2),
("resource_tag_value__in", "value,value2", 2),
("resource_tag_value__icontains", "value", 2),
("resource_tags", "key:value", 2),
("resource_tags", "not:exists", 0),
("resource_tags", "not:exists,key:value", 2),
# full text search on finding tags (disabled for now)
# ("search", "value2", 2),
# Temporary disabled until we implement tag filtering in the UI
# ("resource_tag_key", "key", 2),
# ("resource_tag_key__in", "key,key2", 2),
# ("resource_tag_key__icontains", "key", 2),
# ("resource_tag_value", "value", 2),
# ("resource_tag_value__in", "value,value2", 2),
# ("resource_tag_value__icontains", "value", 2),
# ("resource_tags", "key:value", 2),
# ("resource_tags", "not:exists", 0),
# ("resource_tags", "not:exists,key:value", 2),
]
),
)
@@ -2474,9 +2672,13 @@ class TestFindingViewSet:
filter_value,
expected_count,
):
filters = {f"filter[{filter_name}]": filter_value}
if "inserted_at" not in filter_name:
filters["filter[inserted_at]"] = TODAY
response = authenticated_client.get(
reverse("finding-list"),
{f"filter[{filter_name}]": filter_value},
filters,
)
assert response.status_code == status.HTTP_200_OK
@@ -2485,9 +2687,7 @@ class TestFindingViewSet:
def test_finding_filter_by_scan_id(self, authenticated_client, findings_fixture):
response = authenticated_client.get(
reverse("finding-list"),
{
"filter[scan]": findings_fixture[0].scan.id,
},
{"filter[scan]": findings_fixture[0].scan.id},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
@@ -2510,6 +2710,7 @@ class TestFindingViewSet:
reverse("finding-list"),
{
"filter[provider]": findings_fixture[0].scan.provider.id,
"filter[inserted_at]": TODAY,
},
)
assert response.status_code == status.HTTP_200_OK
@@ -2524,7 +2725,8 @@ class TestFindingViewSet:
"filter[provider.in]": [
findings_fixture[0].scan.provider.id,
findings_fixture[1].scan.provider.id,
]
],
"filter[inserted_at]": TODAY,
},
)
assert response.status_code == status.HTTP_200_OK
@@ -2558,13 +2760,13 @@ class TestFindingViewSet:
)
def test_findings_sort(self, authenticated_client, sort_field):
response = authenticated_client.get(
reverse("finding-list"), {"sort": sort_field}
reverse("finding-list"), {"sort": sort_field, "filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_200_OK
def test_findings_sort_invalid(self, authenticated_client):
response = authenticated_client.get(
reverse("finding-list"), {"sort": "invalid"}
reverse("finding-list"), {"sort": "invalid", "filter[inserted_at]": TODAY}
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["code"] == "invalid"
@@ -2611,7 +2813,8 @@ class TestFindingViewSet:
expected_services = {"ec2", "s3"}
expected_regions = {"eu-west-1", "us-east-1"}
expected_tags = {"key": ["value"], "key2": ["value2"]}
# Temporarily disabled until we implement tag filtering in the UI
# expected_tags = {"key": ["value"], "key2": ["value2"]}
expected_resource_types = {"prowler-test"}
assert data["data"]["type"] == "findings-metadata"
@@ -2621,7 +2824,7 @@ class TestFindingViewSet:
assert (
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
)
assert data["data"]["attributes"]["tags"] == expected_tags
# assert data["data"]["attributes"]["tags"] == expected_tags
def test_findings_metadata_severity_retrieve(
self, authenticated_client, findings_fixture
@@ -2631,14 +2834,15 @@ class TestFindingViewSet:
reverse("finding-metadata"),
{
"filter[severity__in]": ["low", "medium"],
"filter[inserted_at]": finding_1.updated_at.strftime("%Y-%m-%d"),
"filter[inserted_at]": finding_1.inserted_at.strftime("%Y-%m-%d"),
},
)
data = response.json()
expected_services = {"s3"}
expected_regions = {"eu-west-1"}
expected_tags = {"key": ["value"], "key2": ["value2"]}
# Temporary disabled until we implement tag filtering in the UI
# expected_tags = {"key": ["value"], "key2": ["value2"]}
expected_resource_types = {"prowler-test"}
assert data["data"]["type"] == "findings-metadata"
@@ -2648,7 +2852,7 @@ class TestFindingViewSet:
assert (
set(data["data"]["attributes"]["resource_types"]) == expected_resource_types
)
assert data["data"]["attributes"]["tags"] == expected_tags
# assert data["data"]["attributes"]["tags"] == expected_tags
def test_findings_metadata_future_date(self, authenticated_client):
response = authenticated_client.get(
@@ -2660,7 +2864,8 @@ class TestFindingViewSet:
assert data["data"]["id"] is None
assert data["data"]["attributes"]["services"] == []
assert data["data"]["attributes"]["regions"] == []
assert data["data"]["attributes"]["tags"] == {}
# Temporary disabled until we implement tag filtering in the UI
# assert data["data"]["attributes"]["tags"] == {}
assert data["data"]["attributes"]["resource_types"] == []
def test_findings_metadata_invalid_date(self, authenticated_client):
@@ -4280,18 +4485,15 @@ class TestOverviewViewSet:
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_overview_providers_list(
self, authenticated_client, findings_fixture, resources_fixture
self, authenticated_client, scan_summaries_fixture, resources_fixture
):
response = authenticated_client.get(reverse("overview-providers"))
assert response.status_code == status.HTTP_200_OK
# Only findings from one provider
assert len(response.json()["data"]) == 1
assert response.json()["data"][0]["attributes"]["findings"]["total"] == len(
findings_fixture
)
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 0
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 2
assert response.json()["data"][0]["attributes"]["findings"]["manual"] == 0
assert response.json()["data"][0]["attributes"]["findings"]["total"] == 4
assert response.json()["data"][0]["attributes"]["findings"]["pass"] == 2
assert response.json()["data"][0]["attributes"]["findings"]["fail"] == 1
assert response.json()["data"][0]["attributes"]["findings"]["muted"] == 1
assert response.json()["data"][0]["attributes"]["resources"]["total"] == len(
resources_fixture
)
+14 -4
View File
@@ -1,15 +1,25 @@
from datetime import datetime, timezone
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
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 prowler.providers.aws.aws_provider import AwsProvider
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.kubernetes.kubernetes_provider import KubernetesProvider
from rest_framework.exceptions import ValidationError, NotFound
from api.db_router import MainRouter
from api.exceptions import InvitationTokenExpiredException
from api.models import Provider, Invitation
class CustomOAuth2Client(OAuth2Client):
def __init__(self, client_id, secret, *args, **kwargs):
# Remove any duplicate "scope_delimiter" from kwargs
# Bug present in dj-rest-auth after version v7.0.1
# https://github.com/iMerica/dj-rest-auth/issues/673
kwargs.pop("scope_delimiter", None)
super().__init__(client_id, secret, *args, **kwargs)
def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
+1 -1
View File
@@ -106,7 +106,7 @@ def uuid7_end(uuid_obj: UUID, offset_months: int = 1) -> UUID:
Args:
uuid_obj: A UUIDv7 object.
offset_days: Number of months to offset from the given UUID's date. Defaults to 1 to handle if
offset_months: Number of months to offset from the given UUID's date. Defaults to 1 to handle if
partitions are not being used, if so the value will be the one set at FINDINGS_TABLE_PARTITION_MONTHS.
Returns:
+221 -47
View File
@@ -38,7 +38,65 @@ from api.rls import Tenant
# Tokens
class TokenSerializer(TokenObtainPairSerializer):
def generate_tokens(user: User, tenant_id: str) -> dict:
try:
refresh = RefreshToken.for_user(user)
except InvalidKeyError:
# Handle invalid key error
raise ValidationError(
{
"detail": "Token generation failed due to invalid key configuration. Provide valid "
"DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment."
}
)
except Exception as e:
raise ValidationError({"detail": str(e)})
# Post-process the tokens
# Set the tenant_id
refresh["tenant_id"] = tenant_id
# Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a
# way to set the nbf claim
refresh.payload["nbf"] = refresh["iat"]
# Get the access token
access = refresh.access_token
if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]:
update_last_login(None, user)
return {"access": str(access), "refresh": str(refresh)}
class BaseTokenSerializer(TokenObtainPairSerializer):
def custom_validate(self, attrs, social: bool = False):
email = attrs.get("email")
password = attrs.get("password")
tenant_id = str(attrs.get("tenant_id", ""))
# Authenticate user
user = (
User.objects.get(email=email)
if social
else authenticate(username=email, password=password)
)
if user is None:
raise ValidationError("Invalid credentials")
if tenant_id:
if not user.is_member_of_tenant(tenant_id):
raise ValidationError("Tenant does not exist or user is not a member.")
else:
first_membership = user.memberships.order_by("date_joined").first()
if first_membership is None:
raise ValidationError("User has no memberships.")
tenant_id = str(first_membership.tenant_id)
return generate_tokens(user, tenant_id)
class TokenSerializer(BaseTokenSerializer):
email = serializers.EmailField(write_only=True)
password = serializers.CharField(write_only=True)
tenant_id = serializers.UUIDField(
@@ -56,53 +114,25 @@ class TokenSerializer(TokenObtainPairSerializer):
resource_name = "tokens"
def validate(self, attrs):
email = attrs.get("email")
password = attrs.get("password")
tenant_id = str(attrs.get("tenant_id", ""))
return super().custom_validate(attrs)
# Authenticate user
user = authenticate(username=email, password=password)
if user is None:
raise ValidationError("Invalid credentials")
if tenant_id:
if not user.is_member_of_tenant(tenant_id):
raise ValidationError("Tenant does not exist or user is not a member.")
else:
first_membership = user.memberships.order_by("date_joined").first()
if first_membership is None:
raise ValidationError("User has no memberships.")
tenant_id = str(first_membership.tenant_id)
class TokenSocialLoginSerializer(BaseTokenSerializer):
email = serializers.EmailField(write_only=True)
# Generate tokens
try:
refresh = RefreshToken.for_user(user)
except InvalidKeyError:
# Handle invalid key error
raise ValidationError(
{
"detail": "Token generation failed due to invalid key configuration. Provide valid "
"DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment."
}
)
except Exception as e:
raise ValidationError({"detail": str(e)})
# Output tokens
refresh = serializers.CharField(read_only=True)
access = serializers.CharField(read_only=True)
# Post-process the tokens
# Set the tenant_id
refresh["tenant_id"] = tenant_id
class JSONAPIMeta:
resource_name = "tokens"
# Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a
# way to set the nbf claim
refresh.payload["nbf"] = refresh["iat"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields.pop("password", None)
# Get the access token
access = refresh.access_token
if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]:
update_last_login(None, user)
return {"access": str(access), "refresh": str(refresh)}
def validate(self, attrs):
return super().custom_validate(attrs, social=True)
# TODO: Check if we can change the parent class to TokenRefreshSerializer from rest_framework_simplejwt.serializers
@@ -140,6 +170,30 @@ class TokenRefreshSerializer(serializers.Serializer):
raise ValidationError({"refresh": "Invalid or expired token"})
class TokenSwitchTenantSerializer(serializers.Serializer):
tenant_id = serializers.UUIDField(
write_only=True, help_text="The tenant ID for which to request a new token."
)
access = serializers.CharField(read_only=True)
refresh = serializers.CharField(read_only=True)
class JSONAPIMeta:
resource_name = "tokens-switch-tenant"
def validate(self, attrs):
request = self.context["request"]
user = request.user
if not user.is_authenticated:
raise ValidationError("Invalid or expired token.")
tenant_id = str(attrs.get("tenant_id"))
if not user.is_member_of_tenant(tenant_id):
raise ValidationError("Tenant does not exist or user is not a member.")
return generate_tokens(user, tenant_id)
# Base
@@ -691,6 +745,43 @@ class ProviderSerializer(RLSSerializer):
}
class ProviderIncludeSerializer(RLSSerializer):
"""
Serializer for the Provider model.
"""
provider = ProviderEnumSerializerField()
connection = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Provider
fields = [
"id",
"inserted_at",
"updated_at",
"provider",
"uid",
"alias",
"connection",
# "scanner_args",
]
@extend_schema_field(
{
"type": "object",
"properties": {
"connected": {"type": "boolean"},
"last_checked_at": {"type": "string", "format": "date-time"},
},
}
)
def get_connection(self, obj):
return {
"connected": obj.connected,
"last_checked_at": obj.connection_last_checked_at,
}
class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Provider
@@ -753,6 +844,35 @@ class ScanSerializer(RLSSerializer):
]
class ScanIncludeSerializer(RLSSerializer):
trigger = serializers.ChoiceField(
choices=Scan.TriggerChoices.choices, read_only=True
)
state = StateEnumSerializerField(read_only=True)
class Meta:
model = Scan
fields = [
"id",
"name",
"trigger",
"state",
"unique_resource_count",
"progress",
# "scanner_args",
"duration",
"inserted_at",
"started_at",
"completed_at",
"scheduled_at",
"provider",
]
included_serializers = {
"provider": "api.v1.serializers.ProviderIncludeSerializer",
}
class ScanCreateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Scan
@@ -819,6 +939,14 @@ class ScanTaskSerializer(RLSSerializer):
]
class ScanReportSerializer(serializers.Serializer):
id = serializers.CharField(source="scan")
class Meta:
resource_name = "scan-reports"
fields = ["id"]
class ResourceTagSerializer(RLSSerializer):
"""
Serializer for the ResourceTag model
@@ -884,6 +1012,51 @@ class ResourceSerializer(RLSSerializer):
return fields
class ResourceIncludeSerializer(RLSSerializer):
"""
Serializer for the Resource model.
"""
tags = serializers.SerializerMethodField()
type_ = serializers.CharField(read_only=True)
class Meta:
model = Resource
fields = [
"id",
"inserted_at",
"updated_at",
"uid",
"name",
"region",
"service",
"type_",
"tags",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
}
@extend_schema_field(
{
"type": "object",
"description": "Tags associated with the resource",
"example": {"env": "prod", "owner": "johndoe"},
}
)
def get_tags(self, obj):
return obj.get_tags(self.context.get("tenant_id"))
def get_fields(self):
"""`type` is a Python reserved keyword."""
fields = super().get_fields()
type_ = fields.pop("type_")
fields["type"] = type_
return fields
class FindingSerializer(RLSSerializer):
"""
Serializer for the Finding model.
@@ -913,8 +1086,8 @@ class FindingSerializer(RLSSerializer):
]
included_serializers = {
"scan": ScanSerializer,
"resources": ResourceSerializer,
"scan": ScanIncludeSerializer,
"resources": ResourceIncludeSerializer,
}
@@ -933,7 +1106,8 @@ class FindingMetadataSerializer(serializers.Serializer):
resource_types = serializers.ListField(
child=serializers.CharField(), allow_empty=True
)
tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
# Temporarily disabled until we implement tag filtering in the UI
# tags = serializers.JSONField(help_text="Tags are described as key-value pairs.")
class Meta:
resource_name = "findings-metadata"
@@ -1735,7 +1909,7 @@ class OverviewProviderSerializer(serializers.Serializer):
"properties": {
"pass": {"type": "integer"},
"fail": {"type": "integer"},
"manual": {"type": "integer"},
"muted": {"type": "integer"},
"total": {"type": "integer"},
},
}
@@ -1744,7 +1918,7 @@ class OverviewProviderSerializer(serializers.Serializer):
return {
"pass": obj["findings_passed"],
"fail": obj["findings_failed"],
"manual": obj["findings_manual"],
"muted": obj["findings_muted"],
"total": obj["total_findings"],
}
+15 -9
View File
@@ -3,28 +3,31 @@ from drf_spectacular.views import SpectacularRedocView
from rest_framework_nested import routers
from api.v1.views import (
ComplianceOverviewViewSet,
CustomTokenObtainView,
CustomTokenRefreshView,
CustomTokenSwitchTenantView,
FindingViewSet,
MembershipViewSet,
ProviderGroupViewSet,
ProviderGroupProvidersRelationshipView,
ProviderSecretViewSet,
InvitationViewSet,
GithubSocialLoginView,
GoogleSocialLoginView,
InvitationAcceptViewSet,
RoleViewSet,
RoleProviderGroupRelationshipView,
UserRoleRelationshipView,
InvitationViewSet,
MembershipViewSet,
OverviewViewSet,
ComplianceOverviewViewSet,
ProviderGroupProvidersRelationshipView,
ProviderGroupViewSet,
ProviderSecretViewSet,
ProviderViewSet,
ResourceViewSet,
RoleProviderGroupRelationshipView,
RoleViewSet,
ScanViewSet,
ScheduleViewSet,
SchemaView,
TaskViewSet,
TenantMembersViewSet,
TenantViewSet,
UserRoleRelationshipView,
UserViewSet,
)
@@ -56,6 +59,7 @@ users_router.register(r"memberships", MembershipViewSet, basename="user-membersh
urlpatterns = [
path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"),
path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"),
path("tokens/switch", CustomTokenSwitchTenantView.as_view(), name="token-switch"),
path(
"providers/secrets",
ProviderSecretViewSet.as_view({"get": "list", "post": "create"}),
@@ -106,6 +110,8 @@ urlpatterns = [
),
name="provider_group-providers-relationship",
),
path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"),
path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"),
path("", include(router.urls)),
path("", include(tenants_router.urls)),
path("", include(users_router.urls)),
+325 -126
View File
@@ -1,10 +1,23 @@
import glob
import os
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
from celery.result import AsyncResult
from config.env import env
from config.settings.social_login import (
GITHUB_OAUTH_CALLBACK_URL,
GOOGLE_OAUTH_CALLBACK_URL,
)
from dj_rest_auth.registration.views import SocialLoginView
from django.conf import settings as django_settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchQuery
from django.db import transaction
from django.db.models import Count, F, OuterRef, Prefetch, Q, Subquery, Sum
from django.db.models.functions import JSONObject
from django.db.models import Count, Exists, F, OuterRef, Prefetch, Q, Subquery, Sum
from django.db.models.functions import Coalesce
from django.http import HttpResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
@@ -31,11 +44,11 @@ from rest_framework.permissions import SAFE_METHODS
from rest_framework_json_api.views import RelationshipView, Response
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from tasks.beat import schedule_provider_scan
from tasks.jobs.export import get_s3_client
from tasks.tasks import (
check_provider_connection_task,
delete_provider_task,
delete_tenant_task,
perform_scan_summary_task,
perform_scan_task,
)
@@ -68,13 +81,13 @@ from api.models import (
ProviderGroupMembership,
ProviderSecret,
Resource,
ResourceFindingMapping,
Role,
RoleProviderGroupRelationship,
Scan,
ScanSummary,
SeverityChoices,
StateChoices,
StatusChoices,
Task,
User,
UserRoleRelationship,
@@ -82,8 +95,7 @@ from api.models import (
from api.pagination import ComplianceOverviewPagination
from api.rbac.permissions import Permissions, get_providers, get_role
from api.rls import Tenant
from api.utils import validate_invitation
from api.uuid_utils import datetime_to_uuid7
from api.utils import CustomOAuth2Client, validate_invitation
from api.v1.serializers import (
ComplianceOverviewFullSerializer,
ComplianceOverviewSerializer,
@@ -115,6 +127,7 @@ from api.v1.serializers import (
RoleSerializer,
RoleUpdateSerializer,
ScanCreateSerializer,
ScanReportSerializer,
ScanSerializer,
ScanUpdateSerializer,
ScheduleDailyCreateSerializer,
@@ -122,6 +135,8 @@ from api.v1.serializers import (
TenantSerializer,
TokenRefreshSerializer,
TokenSerializer,
TokenSocialLoginSerializer,
TokenSwitchTenantSerializer,
UserCreateSerializer,
UserRoleRelationshipSerializer,
UserSerializer,
@@ -188,13 +203,43 @@ class CustomTokenRefreshView(GenericAPIView):
)
@extend_schema(
tags=["Token"],
summary="Switch tenant using a valid tenant ID",
description="Switch tenant by providing a valid tenant ID. The authenticated user must belong to the tenant.",
)
class CustomTokenSwitchTenantView(GenericAPIView):
permission_classes = [permissions.IsAuthenticated]
resource_name = "tokens-switch-tenant"
serializer_class = TokenSwitchTenantSerializer
http_method_names = ["post"]
def post(self, request):
serializer = TokenSwitchTenantSerializer(
data=request.data, context={"request": request}
)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(
data={
"type": "tokens-switch-tenant",
"attributes": serializer.validated_data,
},
status=status.HTTP_200_OK,
)
@extend_schema(exclude=True)
class SchemaView(SpectacularAPIView):
serializer_class = None
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.2.0"
spectacular_settings.VERSION = "1.5.0"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)
@@ -254,6 +299,58 @@ class SchemaView(SpectacularAPIView):
return super().get(request, *args, **kwargs)
@extend_schema(exclude=True)
class GoogleSocialLoginView(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
client_class = CustomOAuth2Client
callback_url = GOOGLE_OAUTH_CALLBACK_URL
def get_response(self):
original_response = super().get_response()
if self.user and self.user.is_authenticated:
serializer = TokenSocialLoginSerializer(data={"email": self.user.email})
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(
data={
"type": "google-social-tokens",
"attributes": serializer.validated_data,
},
status=status.HTTP_200_OK,
)
return original_response
@extend_schema(exclude=True)
class GithubSocialLoginView(SocialLoginView):
adapter_class = GitHubOAuth2Adapter
client_class = CustomOAuth2Client
callback_url = GITHUB_OAUTH_CALLBACK_URL
def get_response(self):
original_response = super().get_response()
if self.user and self.user.is_authenticated:
serializer = TokenSocialLoginSerializer(data={"email": self.user.email})
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
return Response(
data={
"type": "github-social-tokens",
"attributes": serializer.validated_data,
},
status=status.HTTP_200_OK,
)
return original_response
@extend_schema_view(
list=extend_schema(
tags=["User"],
@@ -1026,6 +1123,18 @@ class ProviderViewSet(BaseRLSViewSet):
request=ScanCreateSerializer,
responses={202: OpenApiResponse(response=TaskSerializer)},
),
report=extend_schema(
tags=["Scan"],
summary="Download ZIP report",
description="Returns a ZIP file containing the requested report",
request=ScanReportSerializer,
responses={
200: OpenApiResponse(description="Report obtained successfully"),
202: OpenApiResponse(description="The task is in progress"),
403: OpenApiResponse(description="There is a problem with credentials"),
404: OpenApiResponse(description="The scan has no reports"),
},
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
@@ -1074,6 +1183,10 @@ class ScanViewSet(BaseRLSViewSet):
return ScanCreateSerializer
elif self.action == "partial_update":
return ScanUpdateSerializer
elif self.action == "report":
if hasattr(self, "response_serializer_class"):
return self.response_serializer_class
return ScanReportSerializer
return super().get_serializer_class()
def partial_update(self, request, *args, **kwargs):
@@ -1091,6 +1204,93 @@ class ScanViewSet(BaseRLSViewSet):
)
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
@action(detail=True, methods=["get"], url_name="report")
def report(self, request, pk=None):
scan_instance = self.get_object()
if scan_instance.state == StateChoices.EXECUTING:
# If the scan is still running, return the task
prowler_task = Task.objects.get(id=scan_instance.task.id)
self.response_serializer_class = TaskSerializer
output_serializer = self.get_serializer(prowler_task)
return Response(
data=output_serializer.data,
status=status.HTTP_202_ACCEPTED,
headers={
"Content-Location": reverse(
"task-detail", kwargs={"pk": output_serializer.data["id"]}
)
},
)
try:
output_celery_task = Task.objects.get(
task_runner_task__task_name="scan-report",
task_runner_task__task_args__contains=pk,
)
self.response_serializer_class = TaskSerializer
output_serializer = self.get_serializer(output_celery_task)
if output_serializer.data["state"] == StateChoices.EXECUTING:
# If the task is still running, return the task
return Response(
data=output_serializer.data,
status=status.HTTP_202_ACCEPTED,
headers={
"Content-Location": reverse(
"task-detail", kwargs={"pk": output_serializer.data["id"]}
)
},
)
except Task.DoesNotExist:
# If the task does not exist, it means that the task is removed from the database
pass
output_location = scan_instance.output_location
if not output_location:
return Response(
{"detail": "The scan has no reports."},
status=status.HTTP_404_NOT_FOUND,
)
if scan_instance.output_location.startswith("s3://"):
try:
s3_client = get_s3_client()
except (ClientError, NoCredentialsError, ParamValidationError):
return Response(
{"detail": "There is a problem with credentials."},
status=status.HTTP_403_FORBIDDEN,
)
bucket_name = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET")
key = output_location[len(f"s3://{bucket_name}/") :]
try:
s3_object = s3_client.get_object(Bucket=bucket_name, Key=key)
except ClientError as e:
error_code = e.response.get("Error", {}).get("Code")
if error_code == "NoSuchKey":
return Response(
{"detail": "The scan has no reports."},
status=status.HTTP_404_NOT_FOUND,
)
return Response(
{"detail": "There is a problem with credentials."},
status=status.HTTP_403_FORBIDDEN,
)
file_content = s3_object["Body"].read()
filename = os.path.basename(output_location.split("/")[-1])
else:
zip_files = glob.glob(output_location)
file_path = zip_files[0]
with open(file_path, "rb") as f:
file_content = f.read()
filename = os.path.basename(file_path)
response = HttpResponse(
file_content, content_type="application/x-zip-compressed"
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
def create(self, request, *args, **kwargs):
input_serializer = self.get_serializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
@@ -1105,10 +1305,6 @@ class ScanViewSet(BaseRLSViewSet):
# Disabled for now
# checks_to_execute=scan.scanner_args.get("checks_to_execute"),
},
link=perform_scan_summary_task.si(
tenant_id=self.request.tenant_id,
scan_id=str(scan.id),
),
)
scan.task_id = task.id
@@ -1272,6 +1468,14 @@ class ResourceViewSet(BaseRLSViewSet):
tags=["Finding"],
summary="List all findings",
description="Retrieve a list of all findings with options for filtering by various criteria.",
parameters=[
OpenApiParameter(
name="filter[inserted_at]",
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
required=True,
type=OpenApiTypes.DATE,
)
],
),
retrieve=extend_schema(
tags=["Finding"],
@@ -1289,32 +1493,44 @@ class ResourceViewSet(BaseRLSViewSet):
tags=["Finding"],
summary="Retrieve metadata values from findings",
description="Fetch unique metadata values from a set of findings. This is useful for dynamic filtering.",
parameters=[
OpenApiParameter(
name="filter[inserted_at]",
description="At least one of the variations of the `filter[inserted_at]` filter must be provided.",
required=True,
type=OpenApiTypes.DATE,
)
],
filters=True,
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
class FindingViewSet(BaseRLSViewSet):
queryset = Finding.objects.all()
queryset = Finding.all_objects.all()
serializer_class = FindingSerializer
prefetch_for_includes = {
"__all__": [],
"resources": [
Prefetch("resources", queryset=Resource.objects.select_related("findings"))
],
"scan": [Prefetch("scan", queryset=Scan.objects.select_related("findings"))],
}
http_method_names = ["get"]
filterset_class = FindingFilter
ordering = ["-id"]
http_method_names = ["get"]
ordering = ["-inserted_at"]
ordering_fields = [
"id",
"status",
"severity",
"check_id",
"inserted_at",
"updated_at",
]
prefetch_for_includes = {
"__all__": [],
"resources": [
Prefetch(
"resources",
queryset=Resource.all_objects.prefetch_related("tags", "findings"),
)
],
"scan": [
Prefetch("scan", queryset=Scan.all_objects.select_related("findings"))
],
}
# RBAC required permissions (implicit -> MANAGE_PROVIDERS enable unlimited visibility or check the visibility of
# the provider through the provider group)
required_permissions = []
@@ -1328,48 +1544,59 @@ class FindingViewSet(BaseRLSViewSet):
return super().get_serializer_class()
def get_queryset(self):
tenant_id = self.request.tenant_id
user_roles = get_role(self.request.user)
if user_roles.unlimited_visibility:
# User has unlimited visibility, return all scans
queryset = Finding.objects.filter(tenant_id=self.request.tenant_id)
# User has unlimited visibility, return all findings
queryset = Finding.all_objects.filter(tenant_id=tenant_id)
else:
# User lacks permission, filter providers based on provider groups associated with the role
queryset = Finding.objects.filter(
# User lacks permission, filter findings based on provider groups associated with the role
queryset = Finding.all_objects.filter(
scan__provider__in=get_providers(user_roles)
)
search_value = self.request.query_params.get("filter[search]", None)
if search_value:
# Django's ORM will build a LEFT JOIN and OUTER JOIN on any "through" tables, resulting in duplicates
# The duplicates then require a `distinct` query
search_query = SearchQuery(
search_value, config="simple", search_type="plain"
)
resource_match = Resource.all_objects.filter(
text_search=search_query,
id__in=ResourceFindingMapping.objects.filter(
resource_id=OuterRef("pk"),
tenant_id=tenant_id,
).values("resource_id"),
)
queryset = queryset.filter(
Q(impact_extended__contains=search_value)
| Q(status_extended__contains=search_value)
| Q(check_id=search_value)
| Q(check_id__icontains=search_value)
| Q(text_search=search_query)
| Q(resources__uid=search_value)
| Q(resources__name=search_value)
| Q(resources__region=search_value)
| Q(resources__service=search_value)
| Q(resources__type=search_value)
| Q(resources__uid__contains=search_value)
| Q(resources__name__contains=search_value)
| Q(resources__region__contains=search_value)
| Q(resources__service__contains=search_value)
| Q(resources__tags__text_search=search_query)
| Q(resources__text_search=search_query)
).distinct()
Q(text_search=search_query) | Q(Exists(resource_match))
)
return queryset
def inserted_at_to_uuidv7(self, inserted_at):
if inserted_at is None:
return None
return datetime_to_uuid7(inserted_at)
def filter_queryset(self, queryset):
# Do not apply filters when retrieving specific finding
if self.action == "retrieve":
return queryset
return super().filter_queryset(queryset)
def list(self, request, *args, **kwargs):
base_qs = self.filter_queryset(self.get_queryset())
paginated_ids = self.paginate_queryset(base_qs.values_list("id", flat=True))
if paginated_ids is not None:
ids = list(paginated_ids)
findings = (
Finding.all_objects.filter(tenant_id=self.request.tenant_id, id__in=ids)
.select_related("scan")
.prefetch_related("resources")
)
# Re-sort in Python to preserve ordering:
findings = sorted(findings, key=lambda x: ids.index(x.id))
serializer = self.get_serializer(findings, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(base_qs, many=True)
return Response(serializer.data)
@action(detail=False, methods=["get"], url_name="findings_services_regions")
def findings_services_regions(self, request):
@@ -1394,48 +1621,35 @@ class FindingViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="metadata")
def metadata(self, request):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
result = filtered_queryset.aggregate(
services=ArrayAgg("resources__service", flat=True, distinct=True),
regions=ArrayAgg("resources__region", flat=True, distinct=True),
tags=ArrayAgg(
JSONObject(
key=F("resources__tags__key"), value=F("resources__tags__value")
),
distinct=True,
filter=Q(resources__tags__key__isnull=False),
),
resource_types=ArrayAgg("resources__type", flat=True, distinct=True),
)
if result["services"] is None:
result["services"] = []
if result["regions"] is None:
result["regions"] = []
if result["regions"] is None:
result["regions"] = []
if result["resource_types"] is None:
result["resource_types"] = []
if result["tags"] is None:
result["tags"] = []
filtered_ids = filtered_queryset.order_by().values("id")
tags_dict = {}
for t in result["tags"]:
key, value = t["key"], t["value"]
if key not in tags_dict:
tags_dict[key] = []
tags_dict[key].append(value)
relevant_resources = Resource.all_objects.filter(
tenant_id=tenant_id, findings__id__in=Subquery(filtered_ids)
).only("service", "region", "type")
result["tags"] = tags_dict
serializer = self.get_serializer(
data=result,
aggregation = relevant_resources.aggregate(
services=ArrayAgg("service", flat=True),
regions=ArrayAgg("region", flat=True),
resource_types=ArrayAgg("type", flat=True),
)
services = sorted(set(aggregation["services"] or []))
regions = sorted({region for region in aggregation["regions"] or [] if region})
resource_types = sorted(set(aggregation["resource_types"] or []))
result = {
"services": services,
"regions": regions,
"resource_types": resource_types,
}
serializer = self.get_serializer(data=result)
serializer.is_valid(raise_exception=True)
return Response(data=serializer.data, status=status.HTTP_200_OK)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema_view(
@@ -1999,68 +2213,53 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="providers")
def providers(self, request):
tenant_id = self.request.tenant_id
# Subquery to get the most recent finding for each uid
latest_finding_ids = (
Finding.objects.filter(
latest_scan_ids = (
Scan.objects.filter(
tenant_id=tenant_id,
uid=OuterRef("uid"),
scan__provider=OuterRef("scan__provider"),
state=StateChoices.COMPLETED,
)
.order_by("-id") # Most recent by id
.values("id")[:1]
.order_by("provider_id", "-inserted_at")
.distinct("provider_id")
.values_list("id", flat=True)
)
# Filter findings to only include the most recent for each uid
recent_findings = Finding.objects.filter(
tenant_id=tenant_id, id__in=Subquery(latest_finding_ids)
)
# Aggregate findings by provider
findings_aggregated = (
recent_findings.values("scan__provider__provider")
ScanSummary.objects.filter(tenant_id=tenant_id, scan_id__in=latest_scan_ids)
.values("scan__provider__provider")
.annotate(
findings_passed=Count("id", filter=Q(status=StatusChoices.PASS.value)),
findings_failed=Count("id", filter=Q(status=StatusChoices.FAIL.value)),
findings_manual=Count(
"id", filter=Q(status=StatusChoices.MANUAL.value)
),
total_findings=Count("id"),
findings_passed=Coalesce(Sum("_pass"), 0),
findings_failed=Coalesce(Sum("fail"), 0),
findings_muted=Coalesce(Sum("muted"), 0),
total_findings=Coalesce(Sum("total"), 0),
)
.order_by("-findings_failed")
)
# Aggregate total resources by provider
resources_aggregated = (
Resource.objects.filter(tenant_id=tenant_id)
.values("provider__provider")
.annotate(total_resources=Count("id"))
)
resources_dict = {
row["provider__provider"]: row["total_resources"]
for row in resources_aggregated
}
# Combine findings and resources data
overview = []
for findings in findings_aggregated:
provider = findings["scan__provider__provider"]
total_resources = next(
(
res["total_resources"]
for res in resources_aggregated
if res["provider__provider"] == provider
),
0,
)
for row in findings_aggregated:
provider_type = row["scan__provider__provider"]
overview.append(
{
"provider": provider,
"total_resources": total_resources,
"total_findings": findings["total_findings"],
"findings_passed": findings["findings_passed"],
"findings_failed": findings["findings_failed"],
"findings_manual": findings["findings_manual"],
"provider": provider_type,
"total_resources": resources_dict.get(provider_type, 0),
"total_findings": row["total_findings"],
"findings_passed": row["findings_passed"],
"findings_failed": row["findings_failed"],
"findings_muted": row["findings_muted"],
}
)
serializer = OverviewProviderSerializer(overview, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(detail=False, methods=["get"], url_name="findings")
@@ -2075,7 +2274,7 @@ class OverviewViewSet(BaseRLSViewSet):
state=StateChoices.COMPLETED,
provider_id=OuterRef("scan__provider_id"),
)
.order_by("-id")
.order_by("-inserted_at")
.values("id")[:1]
)
@@ -2120,7 +2319,7 @@ class OverviewViewSet(BaseRLSViewSet):
state=StateChoices.COMPLETED,
provider_id=OuterRef("scan__provider_id"),
)
.order_by("-id")
.order_by("-inserted_at")
.values("id")[:1]
)
@@ -2156,7 +2355,7 @@ class OverviewViewSet(BaseRLSViewSet):
state=StateChoices.COMPLETED,
provider_id=OuterRef("scan__provider_id"),
)
.order_by("-id")
.order_by("-inserted_at")
.values("id")[:1]
)
+29 -6
View File
@@ -4,6 +4,8 @@ from config.custom_logging import LOGGING # noqa
from config.env import BASE_DIR, env # noqa
from config.settings.celery import * # noqa
from config.settings.partitions import * # noqa
from config.settings.sentry import * # noqa
from config.settings.social_login import * # noqa
SECRET_KEY = env("SECRET_KEY", default="secret")
DEBUG = env.bool("DJANGO_DEBUG", default=False)
@@ -29,6 +31,13 @@ INSTALLED_APPS = [
"django_celery_results",
"django_celery_beat",
"rest_framework_simplejwt.token_blacklist",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.github",
"dj_rest_auth.registration",
"rest_framework.authtoken",
]
MIDDLEWARE = [
@@ -42,8 +51,11 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"api.middleware.APILoggingMiddleware",
"allauth.account.middleware.AccountMiddleware",
]
SITE_ID = 1
CORS_ALLOWED_ORIGINS = ["http://localhost", "http://127.0.0.1"]
ROOT_URLCONF = "config.urls"
@@ -115,12 +127,6 @@ DJANGO_GUID = {
DATABASE_ROUTERS = ["api.db_router.MainRouter"]
# Database connection pool
DB_CP_MIN_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MIN_SIZE", 4)
DB_CP_MAX_SIZE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_SIZE", 10)
DB_CP_MAX_IDLE = env.int("DJANGO_DB_CONNECTION_POOL_MAX_IDLE", 36000)
DB_CP_MAX_LIFETIME = env.int("DJANGO_DB_CONNECTION_POOL_MAX_LIFETIME", 86400)
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
@@ -213,3 +219,20 @@ CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
TESTING = False
FINDINGS_MAX_DAYS_IN_RANGE = env.int("DJANGO_FINDINGS_MAX_DAYS_IN_RANGE", 7)
# API export settings
DJANGO_TMP_OUTPUT_DIRECTORY = env.str(
"DJANGO_TMP_OUTPUT_DIRECTORY", "/tmp/prowler_api_output"
)
DJANGO_FINDINGS_BATCH_SIZE = env.str("DJANGO_FINDINGS_BATCH_SIZE", 1000)
DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID = env.str("DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID", "")
DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY = env.str(
"DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY", ""
)
DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN = env.str("DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN", "")
DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION = env.str("DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION", "")
-16
View File
@@ -13,14 +13,6 @@ DATABASES = {
"PASSWORD": env("POSTGRES_PASSWORD", default="prowler"),
"HOST": env("POSTGRES_HOST", default="postgres-db"),
"PORT": env("POSTGRES_PORT", default="5432"),
"OPTIONS": {
"pool": {
"min_size": DB_CP_MIN_SIZE, # noqa: F405
"max_size": DB_CP_MAX_SIZE, # noqa: F405
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
}
},
},
"admin": {
"ENGINE": "psqlextra.backend",
@@ -29,14 +21,6 @@ DATABASES = {
"PASSWORD": env("POSTGRES_ADMIN_PASSWORD", default="S3cret"),
"HOST": env("POSTGRES_HOST", default="postgres-db"),
"PORT": env("POSTGRES_PORT", default="5432"),
"OPTIONS": {
"pool": {
"min_size": DB_CP_MIN_SIZE, # noqa: F405
"max_size": DB_CP_MAX_SIZE, # noqa: F405
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
}
},
},
}
DATABASES["default"] = DATABASES["prowler_user"]
@@ -14,14 +14,6 @@ DATABASES = {
"PASSWORD": env("POSTGRES_PASSWORD"),
"HOST": env("POSTGRES_HOST"),
"PORT": env("POSTGRES_PORT"),
"OPTIONS": {
"pool": {
"min_size": DB_CP_MIN_SIZE, # noqa: F405
"max_size": DB_CP_MAX_SIZE, # noqa: F405
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
}
},
},
"admin": {
"ENGINE": "psqlextra.backend",
@@ -30,14 +22,6 @@ DATABASES = {
"PASSWORD": env("POSTGRES_ADMIN_PASSWORD"),
"HOST": env("POSTGRES_HOST"),
"PORT": env("POSTGRES_PORT"),
"OPTIONS": {
"pool": {
"min_size": DB_CP_MIN_SIZE, # noqa: F405
"max_size": DB_CP_MAX_SIZE, # noqa: F405
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
}
},
},
}
DATABASES["default"] = DATABASES["prowler_user"]
-8
View File
@@ -13,14 +13,6 @@ DATABASES = {
"PASSWORD": env("POSTGRES_PASSWORD", default="postgres"),
"HOST": env("POSTGRES_HOST", default="localhost"),
"PORT": env("POSTGRES_PORT", default="5432"),
"OPTIONS": {
"pool": {
"min_size": DB_CP_MIN_SIZE, # noqa: F405
"max_size": DB_CP_MAX_SIZE, # noqa: F405
"max_idle": DB_CP_MAX_IDLE, # noqa: F405
"max_lifetime": DB_CP_MAX_LIFETIME, # noqa: F405
}
},
},
}
+54
View File
@@ -0,0 +1,54 @@
import sentry_sdk
from config.env import env
IGNORED_EXCEPTIONS = [
# Authentication Errors from AWS
"InvalidToken",
"AccessDeniedException",
"AuthorizationErrorException",
"UnrecognizedClientException",
"UnauthorizedOperation",
"AuthFailure",
"InvalidClientTokenId",
"AccessDenied",
# Shodan Check
"No Shodan API Key",
# For now we don't want to log the RequestLimitExceeded errors
"RequestLimitExceeded",
"ThrottlingException",
"Rate exceeded",
# The following comes from urllib3
# eu-west-1 -- HTTPClientError[126]: An HTTP Client raised an unhandled exception: AWSHTTPSConnectionPool(host='hostname.s3.eu-west-1.amazonaws.com', port=443): Pool is closed.
"Pool is closed",
]
def before_send(event, hint):
"""
before_send handles the Sentry events in order to sent them or not
"""
# Ignore logs with the ignored_exceptions
# https://docs.python.org/3/library/logging.html#logrecord-objects
if "log_record" in hint:
log_msg = hint["log_record"].msg
log_lvl = hint["log_record"].levelno
# Handle Error events and discard the rest
if log_lvl == 40 and any(ignored in log_msg for ignored in IGNORED_EXCEPTIONS):
return
return event
sentry_sdk.init(
dsn=env.str("DJANGO_SENTRY_DSN", ""),
# Add data like request headers and IP for users,
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
before_send=before_send,
send_default_pii=True,
_experiments={
# Set continuous_profiling_auto_start to True
# to automatically start the profiler on when
# possible.
"continuous_profiling_auto_start": True,
},
)
@@ -0,0 +1,53 @@
from config.env import env
# Google Oauth settings
GOOGLE_OAUTH_CLIENT_ID = env("DJANGO_GOOGLE_OAUTH_CLIENT_ID", default="")
GOOGLE_OAUTH_CLIENT_SECRET = env("DJANGO_GOOGLE_OAUTH_CLIENT_SECRET", default="")
GOOGLE_OAUTH_CALLBACK_URL = env("DJANGO_GOOGLE_OAUTH_CALLBACK_URL", default="")
GITHUB_OAUTH_CLIENT_ID = env("DJANGO_GITHUB_OAUTH_CLIENT_ID", default="")
GITHUB_OAUTH_CLIENT_SECRET = env("DJANGO_GITHUB_OAUTH_CLIENT_SECRET", default="")
GITHUB_OAUTH_CALLBACK_URL = env("DJANGO_GITHUB_OAUTH_CALLBACK_URL", default="")
# Allauth settings
ACCOUNT_LOGIN_METHODS = {"email"} # Use Email / Password authentication
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "none" # Do not require email confirmation
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
REST_AUTH = {
"TOKEN_MODEL": None,
"REST_USE_JWT": True,
}
# django-allauth (social)
# Authenticate if local account with this email address already exists
SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
# Connect local account and social account if local account with that email address already exists
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
SOCIALACCOUNT_ADAPTER = "api.adapters.ProwlerSocialAccountAdapter"
SOCIALACCOUNT_PROVIDERS = {
"google": {
"APP": {
"client_id": GOOGLE_OAUTH_CLIENT_ID,
"secret": GOOGLE_OAUTH_CLIENT_SECRET,
"key": "",
},
"SCOPE": [
"email",
"profile",
],
"AUTH_PARAMS": {
"access_type": "online",
},
},
"github": {
"APP": {
"client_id": GITHUB_OAUTH_CLIENT_ID,
"secret": GITHUB_OAUTH_CLIENT_SECRET,
},
"SCOPE": [
"user",
"read:org",
],
},
}
+1 -1
View File
@@ -486,7 +486,7 @@ def scans_fixture(tenants_fixture, providers_fixture):
name="Scan 1",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.AVAILABLE,
state=StateChoices.COMPLETED,
tenant_id=tenant.id,
started_at="2024-01-02T00:00:00Z",
)
+36 -19
View File
@@ -5,10 +5,14 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
from rest_framework_json_api.serializers import ValidationError
from tasks.tasks import perform_scheduled_scan_task
from api.models import Provider
from api.db_utils import rls_transaction
from api.models import Provider, Scan, StateChoices
def schedule_provider_scan(provider_instance: Provider):
tenant_id = str(provider_instance.tenant_id)
provider_id = str(provider_instance.id)
schedule, _ = IntervalSchedule.objects.get_or_create(
every=24,
period=IntervalSchedule.HOURS,
@@ -17,23 +21,9 @@ def schedule_provider_scan(provider_instance: Provider):
# Create a unique name for the periodic task
task_name = f"scan-perform-scheduled-{provider_instance.id}"
# Schedule the task
_, created = PeriodicTask.objects.get_or_create(
interval=schedule,
name=task_name,
task="scan-perform-scheduled",
kwargs=json.dumps(
{
"tenant_id": str(provider_instance.tenant_id),
"provider_id": str(provider_instance.id),
}
),
one_off=False,
defaults={
"start_time": datetime.now(timezone.utc) + timedelta(hours=24),
},
)
if not created:
if PeriodicTask.objects.filter(
interval=schedule, name=task_name, task="scan-perform-scheduled"
).exists():
raise ValidationError(
[
{
@@ -45,9 +35,36 @@ def schedule_provider_scan(provider_instance: Provider):
]
)
with rls_transaction(tenant_id):
scheduled_scan = Scan.objects.create(
tenant_id=tenant_id,
name="Daily scheduled scan",
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.AVAILABLE,
scheduled_at=datetime.now(timezone.utc),
)
# Schedule the task
periodic_task_instance = PeriodicTask.objects.create(
interval=schedule,
name=task_name,
task="scan-perform-scheduled",
kwargs=json.dumps(
{
"tenant_id": tenant_id,
"provider_id": provider_id,
}
),
one_off=False,
start_time=datetime.now(timezone.utc) + timedelta(hours=24),
)
scheduled_scan.scheduler_task_id = periodic_task_instance.id
scheduled_scan.save()
return perform_scheduled_scan_task.apply_async(
kwargs={
"tenant_id": str(provider_instance.tenant_id),
"provider_id": str(provider_instance.id),
"provider_id": provider_id,
},
)
+156
View File
@@ -0,0 +1,156 @@
import os
import zipfile
import boto3
import config.django.base as base
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
from celery.utils.log import get_task_logger
from django.conf import settings
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_ocsf_file_suffix,
output_file_timestamp,
)
from prowler.lib.outputs.csv.csv import CSV
from prowler.lib.outputs.html.html import HTML
from prowler.lib.outputs.ocsf.ocsf import OCSF
logger = get_task_logger(__name__)
# Predefined mapping for output formats and their configurations
OUTPUT_FORMATS_MAPPING = {
"csv": {
"class": CSV,
"suffix": csv_file_suffix,
"kwargs": {},
},
"json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}},
"html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}},
}
def _compress_output_files(output_directory: str) -> str:
"""
Compress output files from all configured output formats into a ZIP archive.
Args:
output_directory (str): The directory where the output files are located.
The function looks up all known suffixes in OUTPUT_FORMATS_MAPPING
and compresses those files into a single ZIP.
Returns:
str: The full path to the newly created ZIP archive.
"""
zip_path = f"{output_directory}.zip"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for suffix in [config["suffix"] for config in OUTPUT_FORMATS_MAPPING.values()]:
zipf.write(
f"{output_directory}{suffix}",
f"output/{output_directory.split('/')[-1]}{suffix}",
)
return zip_path
def get_s3_client():
"""
Create and return a boto3 S3 client using AWS credentials from environment variables.
This function attempts to initialize an S3 client by reading the AWS access key, secret key,
session token, and region from environment variables. It then validates the client by listing
available S3 buckets. If an error occurs during this process (for example, due to missing or
invalid credentials), it falls back to creating an S3 client without explicitly provided credentials,
which may rely on other configuration sources (e.g., IAM roles).
Returns:
boto3.client: A configured S3 client instance.
Raises:
ClientError, NoCredentialsError, or ParamValidationError if both attempts to create a client fail.
"""
s3_client = None
try:
s3_client = boto3.client(
"s3",
aws_access_key_id=settings.DJANGO_OUTPUT_S3_AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.DJANGO_OUTPUT_S3_AWS_SECRET_ACCESS_KEY,
aws_session_token=settings.DJANGO_OUTPUT_S3_AWS_SESSION_TOKEN,
region_name=settings.DJANGO_OUTPUT_S3_AWS_DEFAULT_REGION,
)
s3_client.list_buckets()
except (ClientError, NoCredentialsError, ParamValidationError, ValueError):
s3_client = boto3.client("s3")
s3_client.list_buckets()
return s3_client
def _upload_to_s3(tenant_id: str, zip_path: str, scan_id: str) -> str:
"""
Upload the specified ZIP file to an S3 bucket.
If the S3 bucket environment variables are not configured,
the function returns None without performing an upload.
Args:
tenant_id (str): The tenant identifier, used as part of the S3 key prefix.
zip_path (str): The local file system path to the ZIP file to be uploaded.
scan_id (str): The scan identifier, used as part of the S3 key prefix.
Returns:
str: The S3 URI of the uploaded file (e.g., "s3://<bucket>/<key>") if successful.
None: If the required environment variables for the S3 bucket are not set.
Raises:
botocore.exceptions.ClientError: If the upload attempt to S3 fails for any reason.
"""
if not base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET:
return
try:
s3 = get_s3_client()
s3_key = f"{tenant_id}/{scan_id}/{os.path.basename(zip_path)}"
s3.upload_file(
Filename=zip_path,
Bucket=base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET,
Key=s3_key,
)
return f"s3://{base.DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET}/{s3_key}"
except (ClientError, NoCredentialsError, ParamValidationError, ValueError) as e:
logger.error(f"S3 upload failed: {str(e)}")
def _generate_output_directory(
output_directory, prowler_provider: object, tenant_id: str, scan_id: str
) -> str:
"""
Generate a file system path for the output directory of a prowler scan.
This function constructs the output directory path by combining a base
temporary output directory, the tenant ID, the scan ID, and details about
the prowler provider along with a timestamp. The resulting path is used to
store the output files of a prowler scan.
Note:
This function depends on one external variable:
- `output_file_timestamp`: A timestamp (as a string) used to uniquely identify the output.
Args:
output_directory (str): The base output directory.
prowler_provider (object): An identifier or descriptor for the prowler provider.
Typically, this is a string indicating the provider (e.g., "aws").
tenant_id (str): The unique identifier for the tenant.
scan_id (str): The unique identifier for the scan.
Returns:
str: The constructed file system path for the prowler scan output directory.
Example:
>>> _generate_output_directory("/tmp", "aws", "tenant-1234", "scan-5678")
'/tmp/tenant-1234/aws/scan-5678/prowler-output-2023-02-15T12:34:56'
"""
path = (
f"{output_directory}/{tenant_id}/{scan_id}/prowler-output-"
f"{prowler_provider}-{output_file_timestamp}"
)
os.makedirs("/".join(path.split("/")[:-1]), exist_ok=True)
return path
+28 -8
View File
@@ -152,6 +152,9 @@ def perform_prowler_scan(
for progress, findings in prowler_scan.scan():
for finding in findings:
if finding is None:
logger.error(f"None finding detected on scan {scan_id}.")
continue
for attempt in range(CELERY_DEADLOCK_ATTEMPTS):
try:
with rls_transaction(tenant_id):
@@ -176,7 +179,10 @@ def perform_prowler_scan(
# Update resource fields if necessary
updated_fields = []
if resource_instance.region != finding.region:
if (
finding.region
and resource_instance.region != finding.region
):
resource_instance.region = finding.region
updated_fields.append("region")
if resource_instance.service != finding.service_name:
@@ -222,8 +228,10 @@ def perform_prowler_scan(
last_first_seen_at = None
if finding_uid not in last_status_cache:
most_recent_finding = (
Finding.objects.filter(uid=finding_uid)
.order_by("-id")
Finding.all_objects.filter(
tenant_id=tenant_id, uid=finding_uid
)
.order_by("-inserted_at")
.values("status", "first_seen_at")
.first()
)
@@ -237,8 +245,11 @@ def perform_prowler_scan(
status = FindingStatus[finding.status]
delta = _create_finding_delta(last_status, status)
# For the findings prior to the change, when a first finding is found with delta!="new" it will be assigned a current date as first_seen_at and the successive findings with the same UID will always get the date of the previous finding.
# For new findings, when a finding (delta="new") is found for the first time, the first_seen_at attribute will be assigned the current date, the following findings will get that date.
# For the findings prior to the change, when a first finding is found with delta!="new" it will be
# assigned a current date as first_seen_at and the successive findings with the same UID will
# always get the date of the previous finding.
# For new findings, when a finding (delta="new") is found for the first time, the first_seen_at
# attribute will be assigned the current date, the following findings will get that date.
if not last_first_seen_at:
last_first_seen_at = datetime.now(tz=timezone.utc)
@@ -333,9 +344,18 @@ def perform_prowler_scan(
total_requirements=compliance["total_requirements"],
)
)
with rls_transaction(tenant_id):
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
try:
with rls_transaction(tenant_id):
ComplianceOverview.objects.bulk_create(
compliance_overview_objects, batch_size=100
)
except Exception as overview_exception:
import sentry_sdk
sentry_sdk.capture_exception(overview_exception)
logger.error(
f"Error storing compliance overview for scan {scan_id}: {overview_exception}"
)
if exception is not None:
raise exception
@@ -372,7 +392,7 @@ def aggregate_findings(tenant_id: str, scan_id: str):
- muted_changed: Muted findings with a delta of 'changed'.
"""
with rls_transaction(tenant_id):
findings = Finding.objects.filter(scan_id=scan_id)
findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
aggregation = findings.values(
"check_id",
+165 -27
View File
@@ -1,15 +1,28 @@
from datetime import datetime, timedelta, timezone
from shutil import rmtree
from celery import shared_task
from celery import chain, shared_task
from celery.utils.log import get_task_logger
from config.celery import RLSTask
from config.django.base import DJANGO_FINDINGS_BATCH_SIZE, DJANGO_TMP_OUTPUT_DIRECTORY
from django_celery_beat.models import PeriodicTask
from tasks.jobs.connection import check_provider_connection
from tasks.jobs.deletion import delete_provider, delete_tenant
from tasks.jobs.export import (
OUTPUT_FORMATS_MAPPING,
_compress_output_files,
_generate_output_directory,
_upload_to_s3,
)
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
from tasks.utils import batched, get_next_execution_datetime
from api.db_utils import rls_transaction
from api.decorators import set_tenant
from api.models import Provider, Scan
from api.models import Finding, Provider, Scan, ScanSummary, StateChoices
from api.utils import initialize_prowler_provider
from prowler.lib.outputs.finding import Finding as FindingOutput
logger = get_task_logger(__name__)
@shared_task(base=RLSTask, name="provider-connection-check")
@@ -29,7 +42,7 @@ def check_provider_connection_task(provider_id: str):
return check_provider_connection(provider_id=provider_id)
@shared_task(base=RLSTask, name="provider-deletion")
@shared_task(base=RLSTask, name="provider-deletion", queue="deletion")
@set_tenant
def delete_provider_task(provider_id: str):
"""
@@ -69,13 +82,22 @@ def perform_scan_task(
Returns:
dict: The result of the scan execution, typically including the status and results of the performed checks.
"""
return perform_prowler_scan(
result = perform_prowler_scan(
tenant_id=tenant_id,
scan_id=scan_id,
provider_id=provider_id,
checks_to_execute=checks_to_execute,
)
chain(
perform_scan_summary_task.si(tenant_id, scan_id),
generate_outputs.si(
scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id
),
).apply_async()
return result
@shared_task(base=RLSTask, bind=True, name="scan-perform-scheduled", queue="scans")
def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
@@ -100,34 +122,49 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
task_id = self.request.id
with rls_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
periodic_task_instance = PeriodicTask.objects.get(
name=f"scan-perform-scheduled-{provider_id}"
)
next_scan_date = datetime.combine(
datetime.now(timezone.utc), periodic_task_instance.start_time.time()
) + timedelta(hours=24)
scan_instance = Scan.objects.create(
next_scan_datetime = get_next_execution_datetime(task_id, provider_id)
scan_instance, _ = Scan.objects.get_or_create(
tenant_id=tenant_id,
name="Daily scheduled scan",
provider=provider_instance,
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
next_scan_at=next_scan_date,
task_id=task_id,
state__in=(StateChoices.SCHEDULED, StateChoices.AVAILABLE),
scheduler_task_id=periodic_task_instance.id,
defaults={"state": StateChoices.SCHEDULED},
)
result = perform_prowler_scan(
tenant_id=tenant_id,
scan_id=str(scan_instance.id),
provider_id=provider_id,
)
perform_scan_summary_task.apply_async(
kwargs={
"tenant_id": tenant_id,
"scan_id": str(scan_instance.id),
}
)
scan_instance.task_id = task_id
scan_instance.save()
try:
result = perform_prowler_scan(
tenant_id=tenant_id,
scan_id=str(scan_instance.id),
provider_id=provider_id,
)
except Exception as e:
raise e
finally:
with rls_transaction(tenant_id):
Scan.objects.get_or_create(
tenant_id=tenant_id,
name="Daily scheduled scan",
provider_id=provider_id,
trigger=Scan.TriggerChoices.SCHEDULED,
state=StateChoices.SCHEDULED,
scheduled_at=next_scan_datetime,
scheduler_task_id=periodic_task_instance.id,
)
chain(
perform_scan_summary_task.si(tenant_id, scan_instance.id),
generate_outputs.si(
scan_id=str(scan_instance.id), provider_id=provider_id, tenant_id=tenant_id
),
).apply_async()
return result
@@ -136,6 +173,107 @@ def perform_scan_summary_task(tenant_id: str, scan_id: str):
return aggregate_findings(tenant_id=tenant_id, scan_id=scan_id)
@shared_task(name="tenant-deletion")
@shared_task(name="tenant-deletion", queue="deletion")
def delete_tenant_task(tenant_id: str):
return delete_tenant(pk=tenant_id)
@shared_task(
base=RLSTask,
name="scan-report",
queue="scan-reports",
)
@set_tenant(keep_tenant=True)
def generate_outputs(scan_id: str, provider_id: str, tenant_id: str):
"""
Process findings in batches and generate output files in multiple formats.
This function retrieves findings associated with a scan, processes them
in batches of 50, and writes each batch to the corresponding output files.
It reuses output writer instances across batches, updates them with each
batch of transformed findings, and uses a flag to indicate when the final
batch is being processed. Finally, the output files are compressed and
uploaded to S3.
Args:
tenant_id (str): The tenant identifier.
scan_id (str): The scan identifier.
provider_id (str): The provider_id id to be used in generating outputs.
"""
# Initialize the prowler provider
prowler_provider = initialize_prowler_provider(Provider.objects.get(id=provider_id))
# Get the provider UID
provider_uid = Provider.objects.get(id=provider_id).uid
# Generate and ensure the output directory exists
output_directory = _generate_output_directory(
DJANGO_TMP_OUTPUT_DIRECTORY, provider_uid, tenant_id, scan_id
)
# Define auxiliary variables
output_writers = {}
scan_summary = FindingOutput._transform_findings_stats(
ScanSummary.objects.filter(scan_id=scan_id)
)
# Retrieve findings queryset
findings_qs = Finding.all_objects.filter(scan_id=scan_id).order_by("uid")
# Process findings in batches
for batch, is_last_batch in batched(
findings_qs.iterator(), DJANGO_FINDINGS_BATCH_SIZE
):
finding_outputs = [
FindingOutput.transform_api_finding(finding, prowler_provider)
for finding in batch
]
# Generate output files
for mode, config in OUTPUT_FORMATS_MAPPING.items():
kwargs = dict(config.get("kwargs", {}))
if mode == "html":
kwargs["provider"] = prowler_provider
kwargs["stats"] = scan_summary
writer_class = config["class"]
if writer_class in output_writers:
writer = output_writers[writer_class]
writer.transform(finding_outputs)
writer.close_file = is_last_batch
else:
writer = writer_class(
findings=finding_outputs,
file_path=output_directory,
file_extension=config["suffix"],
from_cli=False,
)
writer.close_file = is_last_batch
output_writers[writer_class] = writer
# Write the current batch using the writer
writer.batch_write_data_to_file(**kwargs)
# TODO: Refactor the output classes to avoid this manual reset
writer._data = []
# Compress output files
output_directory = _compress_output_files(output_directory)
# Save to configured storage
uploaded = _upload_to_s3(tenant_id, output_directory, scan_id)
if uploaded:
output_directory = uploaded
uploaded = True
# Remove the local files after upload
rmtree(DJANGO_TMP_OUTPUT_DIRECTORY, ignore_errors=True)
else:
uploaded = False
# Update the scan instance with the output path
Scan.all_objects.filter(id=scan_id).update(output_location=output_directory)
logger.info(f"Scan output files generated, output location: {output_directory}")
return {"upload": uploaded}
+4
View File
@@ -6,6 +6,8 @@ from django_celery_beat.models import IntervalSchedule, PeriodicTask
from rest_framework_json_api.serializers import ValidationError
from tasks.beat import schedule_provider_scan
from api.models import Scan
@pytest.mark.django_db
class TestScheduleProviderScan:
@@ -15,9 +17,11 @@ class TestScheduleProviderScan:
with patch(
"tasks.tasks.perform_scheduled_scan_task.apply_async"
) as mock_apply_async:
assert Scan.all_objects.count() == 0
result = schedule_provider_scan(provider_instance)
assert result is not None
assert Scan.all_objects.count() == 1
mock_apply_async.assert_called_once_with(
kwargs={
+102
View File
@@ -0,0 +1,102 @@
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from django_celery_beat.models import IntervalSchedule, PeriodicTask
from django_celery_results.models import TaskResult
from tasks.utils import batched, get_next_execution_datetime
@pytest.mark.django_db
class TestGetNextExecutionDatetime:
@pytest.fixture
def setup_periodic_task(self, db):
# Create a periodic task with an hourly interval
interval = IntervalSchedule.objects.create(
every=1, period=IntervalSchedule.HOURS
)
periodic_task = PeriodicTask.objects.create(
name="scan-perform-scheduled-123",
task="scan-perform-scheduled",
interval=interval,
)
return periodic_task
@pytest.fixture
def setup_task_result(self, db):
# Create a task result record
task_result = TaskResult.objects.create(
task_id="abc123",
task_name="scan-perform-scheduled",
status="SUCCESS",
date_created=datetime.now(timezone.utc) - timedelta(hours=1),
result="Success",
)
return task_result
def test_get_next_execution_datetime_success(
self, setup_task_result, setup_periodic_task
):
task_result = setup_task_result
periodic_task = setup_periodic_task
# Mock periodic_task_name on TaskResult
with patch.object(
TaskResult, "periodic_task_name", return_value=periodic_task.name
):
next_execution = get_next_execution_datetime(
task_id=task_result.task_id, provider_id="123"
)
expected_time = task_result.date_created + timedelta(hours=1)
assert next_execution == expected_time
def test_get_next_execution_datetime_fallback_to_provider_id(
self, setup_task_result, setup_periodic_task
):
task_result = setup_task_result
# Simulate the case where `periodic_task_name` is missing
with patch.object(TaskResult, "periodic_task_name", return_value=None):
next_execution = get_next_execution_datetime(
task_id=task_result.task_id, provider_id="123"
)
expected_time = task_result.date_created + timedelta(hours=1)
assert next_execution == expected_time
def test_get_next_execution_datetime_periodic_task_does_not_exist(
self, setup_task_result
):
task_result = setup_task_result
with pytest.raises(PeriodicTask.DoesNotExist):
get_next_execution_datetime(
task_id=task_result.task_id, provider_id="nonexistent"
)
class TestBatchedFunction:
def test_empty_iterable(self):
result = list(batched([], 3))
assert result == [([], True)]
def test_exact_batches(self):
result = list(batched([1, 2, 3, 4], 2))
expected = [([1, 2], False), ([3, 4], False), ([], True)]
assert result == expected
def test_inexact_batches(self):
result = list(batched([1, 2, 3, 4, 5], 2))
expected = [([1, 2], False), ([3, 4], False), ([5], True)]
assert result == expected
def test_batch_size_one(self):
result = list(batched([1, 2, 3], 1))
expected = [([1], False), ([2], False), ([3], False), ([], True)]
assert result == expected
def test_batch_size_greater_than_length(self):
result = list(batched([1, 2, 3], 5))
expected = [([1, 2, 3], True)]
assert result == expected
+50
View File
@@ -0,0 +1,50 @@
from datetime import datetime, timedelta, timezone
from django_celery_beat.models import PeriodicTask
from django_celery_results.models import TaskResult
def get_next_execution_datetime(task_id: int, provider_id: str) -> datetime:
task_instance = TaskResult.objects.get(task_id=task_id)
try:
periodic_task_instance = PeriodicTask.objects.get(
name=task_instance.periodic_task_name
)
except PeriodicTask.DoesNotExist:
periodic_task_instance = PeriodicTask.objects.get(
name=f"scan-perform-scheduled-{provider_id}"
)
interval = periodic_task_instance.interval
current_scheduled_time = datetime.combine(
datetime.now(timezone.utc).date(),
task_instance.date_created.time(),
tzinfo=timezone.utc,
)
return current_scheduled_time + timedelta(**{interval.period: interval.every})
def batched(iterable, batch_size):
"""
Yield successive batches from an iterable.
Args:
iterable: An iterable source of items.
batch_size (int): The number of items per batch.
Yields:
tuple: A pair (batch, is_last_batch) where:
- batch (list): A list of items (with length equal to batch_size,
except possibly for the last batch).
- is_last_batch (bool): True if this is the final batch, False otherwise.
"""
batch = []
for item in iterable:
batch.append(item)
if len(batch) == batch_size:
yield batch, False
batch = []
yield batch, True
+24
View File
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_cis
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_DESCRIPTION",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
].copy()
return get_section_containers_cis(
aux, "REQUIREMENTS_ID", "REQUIREMENTS_ATTRIBUTES_SECTION"
)
+23
View File
@@ -0,0 +1,23 @@
import warnings
from dashboard.common_methods import get_section_container_iso
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ATTRIBUTES_CATEGORY",
"REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID",
"REQUIREMENTS_ATTRIBUTES_OBJETIVE_NAME",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_container_iso(
aux, "REQUIREMENTS_ATTRIBUTES_CATEGORY", "REQUIREMENTS_ATTRIBUTES_OBJETIVE_ID"
)
+24
View File
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+23
View File
@@ -0,0 +1,23 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+23
View File
@@ -0,0 +1,23 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
@@ -0,0 +1,24 @@
import warnings
from dashboard.common_methods import get_section_containers_format3
warnings.filterwarnings("ignore")
def get_table(data):
aux = data[
[
"REQUIREMENTS_ID",
"REQUIREMENTS_ATTRIBUTES_SECTION",
"REQUIREMENTS_DESCRIPTION",
"CHECKID",
"STATUS",
"REGION",
"ACCOUNTID",
"RESOURCEID",
]
]
return get_section_containers_format3(
aux, "REQUIREMENTS_ATTRIBUTES_SECTION", "REQUIREMENTS_ID"
)
+2 -3
View File
@@ -76,7 +76,6 @@ def load_csv_files(csv_files):
result = result.replace("_AZURE", " - AZURE")
if "KUBERNETES" in result:
result = result.replace("_KUBERNETES", " - KUBERNETES")
result = result[result.find("CIS_") :]
results.append(result)
unique_results = set(results)
@@ -532,8 +531,8 @@ def get_bar_graph(df, column_name):
# Cut the text if it is too long
for i in range(len(colums)):
if len(colums[i]) > 15:
colums[i] = colums[i][:15] + "..."
if len(colums[i]) > 43:
colums[i] = colums[i][:43] + "..."
fig = px.bar(
df,
+16 -4
View File
@@ -165,9 +165,21 @@ else:
)
# For the timestamp, remove the two columns and keep only the date
data["TIMESTAMP"] = pd.to_datetime(data["TIMESTAMP"])
data["ASSESSMENT_TIME"] = data["TIMESTAMP"].dt.strftime("%Y-%m-%d %H:%M:%S")
# Handle findings from v3 outputs
if "FINDING_UNIQUE_ID" in data.columns:
data.rename(columns={"FINDING_UNIQUE_ID": "FINDING_UID"}, inplace=True)
if "ACCOUNT_ID" in data.columns:
data.rename(columns={"ACCOUNT_ID": "ACCOUNT_UID"}, inplace=True)
if "ASSESSMENT_START_TIME" in data.columns:
data.rename(columns={"ASSESSMENT_START_TIME": "TIMESTAMP"}, inplace=True)
if "RESOURCE_ID" in data.columns:
data.rename(columns={"RESOURCE_ID": "RESOURCE_UID"}, inplace=True)
# Remove dupplicates on the finding_uid colummn but keep the last one taking into account the timestamp
data = data.sort_values("TIMESTAMP").drop_duplicates("FINDING_UID", keep="last")
data["ASSESSMENT_TIME"] = data["TIMESTAMP"].dt.strftime("%Y-%m-%d")
data_valid = pd.DataFrame()
for account in data["ACCOUNT_UID"].unique():
all_times = data[data["ACCOUNT_UID"] == account]["ASSESSMENT_TIME"].unique()
@@ -1720,7 +1732,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
[
html.P(
html.Strong(
"Recomendation: ",
"Recommendation: ",
style={
"margin-right": "5px"
},
@@ -1744,7 +1756,7 @@ def generate_table(data, index, color_mapping_severity, color_mapping_status):
[
html.P(
html.Strong(
"RecomendationUrl: ",
"RecommendationUrl: ",
style={
"margin-right": "5px"
},
+6
View File
@@ -16,6 +16,7 @@ services:
volumes:
- "./api/src/backend:/home/prowler/backend"
- "./api/pyproject.toml:/home/prowler/pyproject.toml"
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
depends_on:
postgres:
condition: service_healthy
@@ -35,6 +36,9 @@ services:
required: false
ports:
- 3000:3000
volumes:
- "./ui:/app"
- "/app/node_modules"
postgres:
image: postgres:16.3-alpine3.20
@@ -82,6 +86,8 @@ services:
env_file:
- path: .env
required: false
volumes:
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
depends_on:
valkey:
condition: service_healthy
+4
View File
@@ -7,6 +7,8 @@ services:
required: false
ports:
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
volumes:
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
depends_on:
postgres:
condition: service_healthy
@@ -65,6 +67,8 @@ services:
env_file:
- path: .env
required: false
volumes:
- "/tmp/prowler_api_output:/tmp/prowler_api_output"
depends_on:
valkey:
condition: service_healthy
+6 -1
View File
@@ -19,8 +19,13 @@ For isolation and to avoid conflicts with other environments, we recommend using
Then install all dependencies including the ones for developers:
```
poetry install --with dev
poetry shell
eval $(poetry env activate) \
```
> [!IMPORTANT]
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
>
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
## Contributing with your code or fixes to Prowler
+1
View File
@@ -175,6 +175,7 @@ Due to the complexity and differences of each provider use the rest of the provi
- [GCP](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/gcp_provider.py)
- [Azure](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/azure/azure_provider.py)
- [Kubernetes](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/kubernetes/kubernetes_provider.py)
- [Microsoft365](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/microsoft365/microsoft365_provider.py)
To facilitate understanding here is a pseudocode of how the most basic provider could be with examples.
+1
View File
@@ -237,3 +237,4 @@ It is really important to check if the current Prowler's permissions for each pr
- AWS: https://docs.prowler.cloud/en/latest/getting-started/requirements/#aws-authentication
- Azure: https://docs.prowler.cloud/en/latest/getting-started/requirements/#permissions
- GCP: https://docs.prowler.cloud/en/latest/getting-started/requirements/#gcp-authentication
- Microsoft365: https://docs.prowler.cloud/en/latest/getting-started/requirements/#microsoft365-authentication
+29
View File
@@ -102,3 +102,32 @@ Those credentials must be associated to a user or service account with proper pe
???+ note
By default, `prowler` will scan all accessible GCP Projects, use flag `--project-ids` to specify the projects to be scanned.
## Microsoft365
Prowler for Microsoft365 currently supports the following authentication types:
- [Service principal application](https://learn.microsoft.com/en-us/entra/identity-platform/app-objects-and-service-principals?tabs=browser#service-principal-object) (recommended).
- Current az cli credentials stored.
- Interactive browser authentication.
???+ warning
For Prowler App only the Service Principal with an application authentication method is supported.
### Service Principal authentication
To allow Prowler assume the service principal identity to start the scan it is needed to configure the following environment variables:
```console
export AZURE_CLIENT_ID="XXXXXXXXX"
export AZURE_CLIENT_SECRET="XXXXXXXXX"
export AZURE_TENANT_ID="XXXXXXXXX"
```
If you try to execute Prowler with the `--sp-env-auth` flag and those variables are empty or not exported, the execution is going to fail.
Follow the instructions in the [Create Prowler Service Principal](../tutorials/azure/create-prowler-service-principal.md) section to create a service principal.
### Interactive Browser authentication
To use `--browser-auth` the user needs to authenticate against Azure using the default browser to start the scan, also `--tenant-id` flag is required.
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+43 -5
View File
@@ -76,7 +76,7 @@ Prowler App can be installed in different ways, depending on your environment:
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
poetry shell \
eval $(poetry env activate) \
set -a \
source .env \
docker compose up postgres valkey -d \
@@ -85,6 +85,12 @@ Prowler App can be installed in different ways, depending on your environment:
gunicorn -c config/guniconf.py config.wsgi:application
```
???+ important
Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
> Now, you can access the API documentation at http://localhost:8080/api/v1/docs.
_Commands to run the API Worker_:
@@ -93,7 +99,7 @@ Prowler App can be installed in different ways, depending on your environment:
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
poetry shell \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
@@ -106,7 +112,7 @@ Prowler App can be installed in different ways, depending on your environment:
git clone https://github.com/prowler-cloud/prowler \
cd prowler/api \
poetry install \
poetry shell \
eval $(poetry env activate) \
set -a \
source .env \
cd src/backend \
@@ -164,7 +170,7 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/),
* `Python >= 3.9`
* `Python pip >= 21.0.0`
* AWS, GCP, Azure and/or Kubernetes credentials
* AWS, GCP, Azure, Microsoft365 and/or Kubernetes credentials
_Commands_:
@@ -377,6 +383,19 @@ Go to [http://localhost:3000](http://localhost:3000) after installing the app (s
<img src="img/sign-up-button.png" alt="Sign Up Button" width="320"/>
<img src="img/sign-up.png" alt="Sign Up" width="285"/>
???+ note "User creation and default tenant behavior"
When creating a new user, the behavior depends on whether an invitation is provided:
- **Without an invitation**:
- A new tenant is automatically created.
- The new user is assigned to this tenant.
- A set of **RBAC admin permissions** is generated and assigned to the user for the newly created tenant.
- **With an invitation**: The user is added to the specified tenant with the permissions defined in the invitation.
This mechanism ensures that the first user in a newly created tenant has administrative permissions within that tenant.
#### **Log In**
Log in with your email and password to start using the Prowler App.
@@ -404,7 +423,7 @@ While the scan is running, start exploring the findings in these sections:
### Prowler CLI
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure` or `kubernetes`):
To run Prowler, you will need to specify the provider (e.g `aws`, `gcp`, `azure`, `microsoft365` or `kubernetes`):
???+ note
If no provider specified, AWS will be used for backward compatibility with most of v2 options.
@@ -535,6 +554,7 @@ prowler kubernetes --kubeconfig-file path
For in-cluster execution, you can use the supplied yaml to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
@@ -545,5 +565,23 @@ kubectl logs prowler-XXXXX --namespace prowler-ns
???+ note
By default, `prowler` will scan all namespaces in your active Kubernetes context. Use the flag `--context` to specify the context to be scanned and `--namespaces` to specify the namespaces to be scanned.
#### Microsoft365
With Microsoft365 you need to specify which auth method is going to be used:
```console
# To use service principal authentication
prowler microsoft365 --sp-env-auth
# To use az cli authentication
prowler microsoft365 --az-cli-auth
# To use browser authentication
prowler microsoft365 --browser-auth --tenant-id "XXXXXXXX"
```
See more details about Microsoft365 Authentication in [Requirements](getting-started/requirements.md#microsoft365)
## Prowler v2 Documentation
For **Prowler v2 Documentation**, please check it out [here](https://github.com/prowler-cloud/prowler/blob/8818f47333a0c1c1a457453c87af0ea5b89a385f/README.md).
+6 -1
View File
@@ -27,7 +27,12 @@ cd prowler
pip install poetry
mkdir /tmp/poetry
poetry config cache-dir /tmp/poetry
poetry shell
eval $(poetry env activate)
poetry install
python prowler.py -v
```
> [!IMPORTANT]
> Starting from Poetry v2.0.0, `poetry shell` has been deprecated in favor of `poetry env activate`.
>
> If your poetry version is below 2.0.0 you must keep using `poetry shell` to activate your environment.
> In case you have any doubts, consult the Poetry environment activation guide: https://python-poetry.org/docs/managing-environments/#activating-the-environment
@@ -4,7 +4,7 @@ To allow Prowler assume an identity to start the scan with the required privileg
To create a Service Principal Application you can use the Azure Portal or the Azure CLI.
## From Azure Portal
## From Azure Portal / Entra Admin Center
1. Access to Microsoft Entra ID
2. In the left menu bar, go to "App registrations"
+1
View File
@@ -47,6 +47,7 @@ The following list includes all the AWS checks with configurable variables that
| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String |
| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String |
| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings |
| `elasticache_redis_cluster_backup_enabled` | `minimum_snapshot_retention_period` | Integer |
| `elb_is_in_multiple_az` | `elb_min_azs` | Integer |
| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer |
| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean |
+1 -1
View File
@@ -36,7 +36,7 @@ In this page you can do multiple functions:
* Severity
* Service
* Status
* See wich files has been scanned to generate the dashboard placing your mouse on the `?` icon:
* See which files has been scanned to generate the dashboard placing your mouse on the `?` icon:
<img src="../img/dashboard/dashboard-files-scanned.png">
* Download the `Top Findings by Severity` table using the button `DOWNLOAD THIS TABLE AS CSV` or `DOWNLOAD THIS TABLE AS XLSX`
* Click on the provider cards to filter by provider.
+3 -20
View File
@@ -25,6 +25,9 @@ Prowler will follow the same credentials search as [Google authentication librar
Those credentials must be associated to a user or service account with proper permissions to do all checks. To make sure, add the `Viewer` role to the member associated with the credentials.
???+ note
Prowler will use the enabled Google Cloud APIs to get the information needed to perform the checks.
## Impersonate Service Account
If you want to impersonate a GCP service account, you can use the `--impersonate-service-account` argument:
@@ -34,23 +37,3 @@ prowler gcp --impersonate-service-account <service-account-email>
```
This argument will use the default credentials to impersonate the service account provided.
## Service APIs
Prowler will use the Google Cloud APIs to get the information needed to perform the checks. Make sure that the following APIs are enabled in the project:
- apikeys.googleapis.com
- artifactregistry.googleapis.com
- bigquery.googleapis.com
- sqladmin.googleapis.com
- storage.googleapis.com
- compute.googleapis.com
- dataproc.googleapis.com
- dns.googleapis.com
- containerregistry.googleapis.com
- container.googleapis.com
- iam.googleapis.com
- cloudkms.googleapis.com
- logging.googleapis.com
You can enable them automatically using our script [enable_apis_in_projects.sh](https://github.com/prowler-cloud/prowler/blob/master/contrib/gcp/enable_apis_in_projects.sh)
+2
View File
@@ -2,6 +2,7 @@
For in-cluster execution, you can use the supplied yaml files inside `/kubernetes`:
* [prowler-sa.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-sa.yaml)
* [job.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/job.yaml)
* [prowler-role.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-role.yaml)
* [prowler-rolebinding.yaml](https://github.com/prowler-cloud/prowler/blob/master/kubernetes/prowler-rolebinding.yaml)
@@ -9,6 +10,7 @@ For in-cluster execution, you can use the supplied yaml files inside `/kubernete
They can be used to run Prowler as a job within a new Prowler namespace:
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/job.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
@@ -0,0 +1,23 @@
# Microsoft365 authentication
By default Prowler uses MsGraph Python SDK identity package authentication methods using the class `ClientSecretCredential`.
This allows Prowler to authenticate against microsoft365 using the following methods:
- Service principal authentication by environment variables (Enterprise Application)
- Current CLI credentials stored
- Interactive browser authentication
To launch the tool first you need to specify which method is used through the following flags:
```console
# To use service principal authentication
prowler microsoft365 --sp-env-auth
# To use cli authentication
prowler microsoft365 --az-cli-auth
# To use browser authentication
prowler microsoft365 --browser-auth --tenant-id "XXXXXXXX"
```
To use Prowler you need to set up also the permissions required to access your resources in your Microsoft365 account, to more details refer to [Requirements](../../getting-started/requirements.md)
+39
View File
@@ -71,6 +71,15 @@ For AWS, enter your `AWS Account ID` and choose one of the following methods to
<img src="../../img/aws-role.png" alt="AWS Role" width="700"/>
???+ note
check if your AWS Security Token Service (STS) has the EU (Ireland) endpoint active. If not we will not be able to connect to your AWS account.
If that is the case your STS configuration may look like this:
<img src="../../img/sts-configuration.png" alt="AWS Role" width="800"/>
To solve this issue, please activate the EU (Ireland) STS endpoint.
---
### **Step 4.2: Azure Credentials**
@@ -99,6 +108,34 @@ By default, the `kubeconfig` file is located at `~/.kube/config`.
<img src="../../img/kubernetes-credentials.png" alt="Kubernetes Credentials" width="700"/>
???+ note
If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these additional steps to ensure proper authentication:
** Make sure your cluster allow traffic from the Prowler Cloud IP address `52.48.254.174/32` **
1. Apply the necessary Kubernetes resources to your EKS, GKE, AKS or external cluster (you can find the files in the [`kubernetes` directory of the Prowler repository](https://github.com/prowler-cloud/prowler/tree/master/kubernetes)):
```console
kubectl apply -f kubernetes/prowler-sa.yaml
kubectl apply -f kubernetes/prowler-role.yaml
kubectl apply -f kubernetes/prowler-rolebinding.yaml
```
2. Generate a long-lived token for authentication:
```console
kubectl create token prowler-sa -n prowler-ns --duration=0
```
- **Security Note:** The `--duration=0` option generates a non-expiring token, which may pose a security risk if not managed properly. Users should decide on an appropriate expiration time based on their security policies. If a limited-time token is preferred, set `--duration=<TIME>` (e.g., `--duration=24h`).
- **Important:** If the token expires, Prowler Cloud will no longer be able to authenticate with the cluster. In this case, you will need to generate a new token and **remove and re-add the provider in Prowler Cloud** with the updated `kubeconfig`.
3. Update your `kubeconfig` to use the ServiceAccount token:
```console
kubectl config set-credentials prowler-sa --token=<SA_TOKEN>
kubectl config set-context <CONTEXT_NAME> --user=prowler-sa
```
Replace <SA_TOKEN> with the generated token and <CONTEXT_NAME> with your KubeConfig Context Name of your EKS, GKE or AKS cluster.
4. Now you can add the modified `kubeconfig` in Prowler Cloud. Then simply test the connection.
---
## **Step 5: Test Connection**
@@ -133,3 +170,5 @@ While the scan is running, start exploring the findings in these sections:
<img src="../../img/issues.png" alt="Issues" width="300" style="text-align: center;"/>
- **Browse All Findings**: Detailed list of findings detected, where you can filter by severity, service, and more. <img src="../../img/findings.png" alt="Findings" width="700"/>
To view all `new` findings that have not been seen prior to this scan, click the `Delta` filter and select `new`. To view all `changed` findings that have had a status change (from `PASS` to `FAIL` for example), click the `Delta` filter and select `changed`.
+5
View File
@@ -0,0 +1,5 @@
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;accessanalyzer_enabled;Check if IAM Access Analyzer is enabled;IAM;FAIL;IAM Access Analyzer in account <account_uid> is not enabled.;False;accessanalyzer;;low;Other;<resource_uid>;<resource_name>;;;aws;<region>;Check if IAM Access Analyzer is enabled;AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy.;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost).;https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html;;;aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION>;;CIS-1.4: 1.20 | CIS-1.5: 1.20 | KISA-ISMS-P-2023: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | CIS-2.0: 1.20 | KISA-ISMS-P-2023-korean: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | AWS-Account-Security-Onboarding: Enabled security services, Create analyzers in each active regions, Verify that events are present in SecurityHub aggregated view | CIS-3.0: 1.20;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_current_contact_details;Maintain current contact details.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain current contact details.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;CIS-1.4: 1.1 | CIS-1.5: 1.1 | KISA-ISMS-P-2023: 2.1.3 | CIS-2.0: 1.1 | KISA-ISMS-P-2023-korean: 2.1.3 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.1 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_maintain_different_contact_details_to_security_billing_and_operations;Maintain different contact details to security, billing and operations.;IAM;FAIL;SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.;False;account;;medium;Other;<resource_uid>;<account_uid>;;;aws;<region>;Maintain different contact details to security, billing and operations.;Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;Using the Billing and Cost Management console complete contact details.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;;https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console;KISA-ISMS-P-2023: 2.1.3 | KISA-ISMS-P-2023-korean: 2.1.3;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:03.913874;<account_uid>;;;;;;<finding_uid>;aws;account_security_contact_information_is_registered;Ensure security contact information is registered.;IAM;MANUAL;Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.;False;account;;medium;Other;<resource_uid>:root;<account_uid>;;;aws;<region>;Ensure security contact information is registered.;AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them.;;Go to the My Account section and complete alternate contacts.;https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html;;;No command available.;https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console;CIS-1.4: 1.2 | CIS-1.5: 1.2 | AWS-Foundational-Security-Best-Practices: account, acm | KISA-ISMS-P-2023: 2.1.3, 2.2.1 | CIS-2.0: 1.2 | KISA-ISMS-P-2023-korean: 2.1.3, 2.2.1 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.2 | ENS-RD2022: op.ext.7.aws.am.1;;;;;<prowler_version>
1 AUTH_METHOD TIMESTAMP ACCOUNT_UID ACCOUNT_NAME ACCOUNT_EMAIL ACCOUNT_ORGANIZATION_UID ACCOUNT_ORGANIZATION_NAME ACCOUNT_TAGS FINDING_UID PROVIDER CHECK_ID CHECK_TITLE CHECK_TYPE STATUS STATUS_EXTENDED MUTED SERVICE_NAME SUBSERVICE_NAME SEVERITY RESOURCE_TYPE RESOURCE_UID RESOURCE_NAME RESOURCE_DETAILS RESOURCE_TAGS PARTITION REGION DESCRIPTION RISK RELATED_URL REMEDIATION_RECOMMENDATION_TEXT REMEDIATION_RECOMMENDATION_URL REMEDIATION_CODE_NATIVEIAC REMEDIATION_CODE_TERRAFORM REMEDIATION_CODE_CLI REMEDIATION_CODE_OTHER COMPLIANCE CATEGORIES DEPENDS_ON RELATED_TO NOTES PROWLER_VERSION
2 <auth_method> 2025-02-14 14:27:03.913874 <account_uid> <finding_uid> aws accessanalyzer_enabled Check if IAM Access Analyzer is enabled IAM FAIL IAM Access Analyzer in account <account_uid> is not enabled. False accessanalyzer low Other <resource_uid> <resource_name> aws <region> Check if IAM Access Analyzer is enabled AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy. https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost). https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION> CIS-1.4: 1.20 | CIS-1.5: 1.20 | KISA-ISMS-P-2023: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | CIS-2.0: 1.20 | KISA-ISMS-P-2023-korean: 2.5.6, 2.6.4, 2.8.1, 2.8.2 | AWS-Account-Security-Onboarding: Enabled security services, Create analyzers in each active regions, Verify that events are present in SecurityHub aggregated view | CIS-3.0: 1.20 <prowler_version>
3 <auth_method> 2025-02-14 14:27:03.913874 <account_uid> <finding_uid> aws account_maintain_current_contact_details Maintain current contact details. IAM MANUAL Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information. False account medium Other <resource_uid> <account_uid> aws <region> Maintain current contact details. Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question. Using the Billing and Cost Management console complete contact details. https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html No command available. https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console CIS-1.4: 1.1 | CIS-1.5: 1.1 | KISA-ISMS-P-2023: 2.1.3 | CIS-2.0: 1.1 | KISA-ISMS-P-2023-korean: 2.1.3 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.1 | ENS-RD2022: op.ext.7.aws.am.1 <prowler_version>
4 <auth_method> 2025-02-14 14:27:03.913874 <account_uid> <finding_uid> aws account_maintain_different_contact_details_to_security_billing_and_operations Maintain different contact details to security, billing and operations. IAM FAIL SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact. False account medium Other <resource_uid> <account_uid> aws <region> Maintain different contact details to security, billing and operations. Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question. https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html Using the Billing and Cost Management console complete contact details. https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console KISA-ISMS-P-2023: 2.1.3 | KISA-ISMS-P-2023-korean: 2.1.3 <prowler_version>
5 <auth_method> 2025-02-14 14:27:03.913874 <account_uid> <finding_uid> aws account_security_contact_information_is_registered Ensure security contact information is registered. IAM MANUAL Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section. False account medium Other <resource_uid>:root <account_uid> aws <region> Ensure security contact information is registered. AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them. Go to the My Account section and complete alternate contacts. https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html No command available. https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console CIS-1.4: 1.2 | CIS-1.5: 1.2 | AWS-Foundational-Security-Best-Practices: account, acm | KISA-ISMS-P-2023: 2.1.3, 2.2.1 | CIS-2.0: 1.2 | KISA-ISMS-P-2023-korean: 2.1.3, 2.2.1 | AWS-Well-Architected-Framework-Security-Pillar: SEC03-BP03, SEC10-BP01 | AWS-Account-Security-Onboarding: Billing, emergency, security contacts | CIS-3.0: 1.2 | ENS-RD2022: op.ext.7.aws.am.1 <prowler_version>
@@ -0,0 +1,625 @@
[
{
"message": "IAM Access Analyzer in account <account_uid> is not enabled.",
"metadata": {
"event_code": "accessanalyzer_enabled",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "<prowler_version>"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "",
"version": "1.4.0"
},
"severity_id": 2,
"severity": "Low",
"status": "New",
"status_code": "FAIL",
"status_detail": "IAM Access Analyzer in account <account_uid> is not enabled.",
"status_id": 1,
"unmapped": {
"related_url": "https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-1.4": [
"1.20"
],
"CIS-1.5": [
"1.20"
],
"KISA-ISMS-P-2023": [
"2.5.6",
"2.6.4",
"2.8.1",
"2.8.2"
],
"CIS-2.0": [
"1.20"
],
"KISA-ISMS-P-2023-korean": [
"2.5.6",
"2.6.4",
"2.8.1",
"2.8.2"
],
"AWS-Account-Security-Onboarding": [
"Enabled security services",
"Create analyzers in each active regions",
"Verify that events are present in SecurityHub aggregated view"
],
"CIS-3.0": [
"1.20"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539623,
"created_time_dt": "2025-02-14T14:27:03.913874",
"desc": "Check if IAM Access Analyzer is enabled",
"product_uid": "prowler",
"title": "Check if IAM Access Analyzer is enabled",
"types": [
"IAM"
],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "aws",
"region": "<region>",
"data": {
"details": "",
"metadata": {
"arn": "<resource_arn>",
"name": "<resource_name>",
"status": "NOT_AVAILABLE",
"findings": [],
"tags": [],
"type": "",
"region": "<region>"
}
},
"group": {
"name": "accessanalyzer"
},
"labels": [],
"name": "<resource_name>",
"type": "Other",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "",
"type": "AWS Account",
"type_id": 10,
"uid": "<account_uid>",
"labels": []
},
"org": {
"name": "",
"uid": ""
},
"provider": "aws",
"region": "<region>"
},
"remediation": {
"desc": "Enable IAM Access Analyzer for all accounts, create analyzer and take action over it is recommendations (IAM Access Analyzer is available at no additional cost).",
"references": [
"aws accessanalyzer create-analyzer --analyzer-name <NAME> --type <ACCOUNT|ORGANIZATION>",
"https://docs.aws.amazon.com/IAM/latest/UserGuide/what-is-access-analyzer.html"
]
},
"risk_details": "AWS IAM Access Analyzer helps you identify the resources in your organization and accounts, such as Amazon S3 buckets or IAM roles, that are shared with an external entity. This lets you identify unintended access to your resources and data, which is a security risk. IAM Access Analyzer uses a form of mathematical analysis called automated reasoning, which applies logic and mathematical inference to determine all possible access paths allowed by a resource policy.",
"time": 1739539623,
"time_dt": "2025-02-14T14:27:03.913874",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.",
"metadata": {
"event_code": "account_maintain_current_contact_details",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "<prowler_version>"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "MANUAL",
"status_detail": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Contact Information.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-1.4": [
"1.1"
],
"CIS-1.5": [
"1.1"
],
"KISA-ISMS-P-2023": [
"2.1.3"
],
"CIS-2.0": [
"1.1"
],
"KISA-ISMS-P-2023-korean": [
"2.1.3"
],
"AWS-Well-Architected-Framework-Security-Pillar": [
"SEC03-BP03",
"SEC10-BP01"
],
"AWS-Account-Security-Onboarding": [
"Billing, emergency, security contacts"
],
"CIS-3.0": [
"1.1"
],
"ENS-RD2022": [
"op.ext.7.aws.am.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539623,
"created_time_dt": "2025-02-14T14:27:03.913874",
"desc": "Maintain current contact details.",
"product_uid": "prowler",
"title": "Maintain current contact details.",
"types": [
"IAM"
],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "aws",
"region": "<region>",
"data": {
"details": "",
"metadata": {
"type": "PRIMARY",
"email": null,
"name": "<account_name>",
"phone_number": "<value>"
}
},
"group": {
"name": "account"
},
"labels": [],
"name": "<account_uid>",
"type": "Other",
"uid": "arn:aws:iam::<account_uid>:root"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "",
"type": "AWS Account",
"type_id": 10,
"uid": "<account_uid>",
"labels": []
},
"org": {
"name": "",
"uid": ""
},
"provider": "aws",
"region": "<region>"
},
"remediation": {
"desc": "Using the Billing and Cost Management console complete contact details.",
"references": [
"No command available.",
"https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
]
},
"risk_details": "Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.",
"time": 1739539623,
"time_dt": "2025-02-14T14:27:03.913874",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.",
"metadata": {
"event_code": "account_maintain_different_contact_details_to_security_billing_and_operations",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "<prowler_version>"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "SECURITY, BILLING and OPERATIONS contacts not found or they are not different between each other and between ROOT contact.",
"status_id": 1,
"unmapped": {
"related_url": "https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"KISA-ISMS-P-2023": [
"2.1.3"
],
"KISA-ISMS-P-2023-korean": [
"2.1.3"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539623,
"created_time_dt": "2025-02-14T14:27:03.913874",
"desc": "Maintain different contact details to security, billing and operations.",
"product_uid": "prowler",
"title": "Maintain different contact details to security, billing and operations.",
"types": [
"IAM"
],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "aws",
"region": "<region>",
"data": {
"details": "",
"metadata": {
"type": "PRIMARY",
"email": null,
"name": "<account_name>",
"phone_number": "<value>"
}
},
"group": {
"name": "account"
},
"labels": [],
"name": "<account_uid>",
"type": "Other",
"uid": "arn:aws:iam::<account_uid>:root"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "",
"type": "AWS Account",
"type_id": 10,
"uid": "<account_uid>",
"labels": []
},
"org": {
"name": "",
"uid": ""
},
"provider": "aws",
"region": "<region>"
},
"remediation": {
"desc": "Using the Billing and Cost Management console complete contact details.",
"references": [
"https://docs.prowler.com/checks/aws/iam-policies/iam_18-maintain-contact-details#aws-console",
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
]
},
"risk_details": "Ensure contact email and telephone details for AWS accounts are current and map to more than one individual in your organization. An AWS account supports a number of contact details, and AWS will use these to contact the account owner if activity judged to be in breach of Acceptable Use Policy. If an AWS account is observed to be behaving in a prohibited or suspicious manner, AWS will attempt to contact the account owner by email and phone using the contact details listed. If this is unsuccessful and the account behavior needs urgent mitigation, proactive measures may be taken, including throttling of traffic between the account exhibiting suspicious behavior and the AWS API endpoints and the Internet. This will result in impaired service to and from the account in question.",
"time": 1739539623,
"time_dt": "2025-02-14T14:27:03.913874",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.",
"metadata": {
"event_code": "account_security_contact_information_is_registered",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "<prowler_version>"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "MANUAL",
"status_detail": "Login to the AWS Console. Choose your account name on the top right of the window -> My Account -> Alternate Contacts -> Security Section.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-1.4": [
"1.2"
],
"CIS-1.5": [
"1.2"
],
"AWS-Foundational-Security-Best-Practices": [
"account",
"acm"
],
"KISA-ISMS-P-2023": [
"2.1.3",
"2.2.1"
],
"CIS-2.0": [
"1.2"
],
"KISA-ISMS-P-2023-korean": [
"2.1.3",
"2.2.1"
],
"AWS-Well-Architected-Framework-Security-Pillar": [
"SEC03-BP03",
"SEC10-BP01"
],
"AWS-Account-Security-Onboarding": [
"Billing, emergency, security contacts"
],
"CIS-3.0": [
"1.2"
],
"ENS-RD2022": [
"op.ext.7.aws.am.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539623,
"created_time_dt": "2025-02-14T14:27:03.913874",
"desc": "Ensure security contact information is registered.",
"product_uid": "prowler",
"title": "Ensure security contact information is registered.",
"types": [
"IAM"
],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "aws",
"region": "<region>",
"data": {
"details": "",
"metadata": {
"type": "PRIMARY",
"email": null,
"name": "<account_name>",
"phone_number": "<value>"
}
},
"group": {
"name": "account"
},
"labels": [],
"name": "<account_uid>",
"type": "Other",
"uid": "arn:aws:iam::<account_uid>:root"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "",
"type": "AWS Account",
"type_id": 10,
"uid": "<account_uid>",
"labels": []
},
"org": {
"name": "",
"uid": ""
},
"provider": "aws",
"region": "<region>"
},
"remediation": {
"desc": "Go to the My Account section and complete alternate contacts.",
"references": [
"No command available.",
"https://docs.prowler.com/checks/aws/iam-policies/iam_19#aws-console",
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-update-contact.html"
]
},
"risk_details": "AWS provides customers with the option of specifying the contact information for accounts security team. It is recommended that this information be provided. Specifying security-specific contact information will help ensure that security advisories sent by AWS reach the team in your organization that is best equipped to respond to them.",
"time": 1739539623,
"time_dt": "2025-02-14T14:27:03.913874",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Login to the AWS Console as root. Choose your account name on the top right of the window -> My Account -> Configure Security Challenge Questions.",
"metadata": {
"event_code": "account_security_questions_are_registered_in_the_aws_account",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "<prowler_version>"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "MANUAL",
"status_detail": "Login to the AWS Console as root. Choose your account name on the top right of the window -> My Account -> Configure Security Challenge Questions.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-1.4": [
"1.3"
],
"CIS-1.5": [
"1.3"
],
"KISA-ISMS-P-2023": [
"2.1.3"
],
"CIS-2.0": [
"1.3"
],
"KISA-ISMS-P-2023-korean": [
"2.1.3"
],
"AWS-Well-Architected-Framework-Security-Pillar": [
"SEC03-BP03",
"SEC10-BP01"
],
"CIS-3.0": [
"1.3"
],
"ENS-RD2022": [
"op.ext.7.aws.am.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539623,
"created_time_dt": "2025-02-14T14:27:03.913874",
"desc": "Ensure security questions are registered in the AWS account.",
"product_uid": "prowler",
"title": "Ensure security questions are registered in the AWS account.",
"types": [
"IAM"
],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "aws",
"region": "<region>",
"data": {
"details": "",
"metadata": {
"type": "SECURITY",
"email": null,
"name": null,
"phone_number": null
}
},
"group": {
"name": "account"
},
"labels": [],
"name": "<account_uid>",
"type": "Other",
"uid": "arn:aws:iam::<account_uid>:root"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "",
"type": "AWS Account",
"type_id": 10,
"uid": "<account_uid>",
"labels": []
},
"org": {
"name": "",
"uid": ""
},
"provider": "aws",
"region": "<region>"
},
"remediation": {
"desc": "Login as root account and from My Account configure Security questions.",
"references": [
"No command available.",
"https://docs.prowler.com/checks/aws/iam-policies/iam_15",
"https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-security-challenge.html"
]
},
"risk_details": "The AWS support portal allows account owners to establish security questions that can be used to authenticate individuals calling AWS customer service for support. It is recommended that security questions be established. When creating a new AWS account a default super user is automatically created. This account is referred to as the root account. It is recommended that the use of this account be limited and highly controlled. During events in which the root password is no longer accessible or the MFA token associated with root is lost/destroyed it is possible through authentication using secret questions and associated answers to recover root login access.",
"time": 1739539623,
"time_dt": "2025-02-14T14:27:03.913874",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
}
]
+5
View File
@@ -0,0 +1,5 @@
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_cluster_rbac_enabled;Ensure AKS RBAC is enabled;;PASS;RBAC is enabled for cluster '<resource_name>' in subscription '<account_name>'.;False;aks;;medium;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Azure Kubernetes Service (AKS) can be configured to use Azure Active Directory (AD) for user authentication. In this configuration, you sign in to an AKS cluster using an Azure AD authentication token. You can also configure Kubernetes role-based access control (Kubernetes RBAC) to limit access to cluster resources based a user's identity or group membership.;Kubernetes RBAC and AKS help you secure your cluster access and provide only the minimum required permissions to developers and operators.;https://learn.microsoft.com/en-us/azure/aks/azure-ad-rbac?tabs=portal;;https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle;;https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_2#terraform;;https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-role-based-access-control-for-kubernetes-service.html#;ENS-RD2022: op.acc.2.az.r1.eid.1;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_clusters_created_with_private_nodes;Ensure clusters are created with Private Nodes;;PASS;Cluster '<resource_name>' was created with private nodes in subscription '<account_name>';False;aks;;high;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Disable public IP addresses for cluster nodes, so that they only have private IP addresses. Private Nodes are nodes with no public IP addresses.;Disabling public IP addresses on cluster nodes restricts access to only internal networks, forcing attackers to obtain local network access before attempting to compromise the underlying Kubernetes hosts.;https://learn.microsoft.com/en-us/azure/aks/private-clusters;;https://learn.microsoft.com/en-us/azure/aks/access-private-cluster;;;;;ENS-RD2022: mp.com.4.r2.az.aks.1 | MITRE-ATTACK: T1190, T1530;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_clusters_public_access_disabled;Ensure clusters are created with Private Endpoint Enabled and Public Access Disabled;;FAIL;Public access to nodes is enabled for cluster '<resource_name>' in subscription '<account_name>';False;aks;;high;Microsoft.ContainerService/ManagedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;Disable access to the Kubernetes API from outside the node network if it is not required.;In a private cluster, the master node has two endpoints, a private and public endpoint. The private endpoint is the internal IP address of the master, behind an internal load balancer in the master's wirtual network. Nodes communicate with the master using the private endpoint. The public endpoint enables the Kubernetes API to be accessed from outside the master's virtual network. Although Kubernetes API requires an authorized token to perform sensitive actions, a vulnerability could potentially expose the Kubernetes publically with unrestricted access. Additionally, an attacker may be able to identify the current cluster and Kubernetes API version and determine whether it is vulnerable to an attack. Unless required, disabling public endpoint will help prevent such threats, and require the attacker to be on the master's virtual network to perform any attack on the Kubernetes API.;https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=azure-portal;To use a private endpoint, create a new private endpoint in your virtual network then create a link between your virtual network and a new private DNS zone;https://learn.microsoft.com/en-us/azure/aks/access-private-cluster?tabs=azure-cli;;;az aks update -n <cluster_name> -g <resource_group> --disable-public-fqdn;;ENS-RD2022: mp.com.4.az.aks.2 | MITRE-ATTACK: T1190, T1530;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:30.710664;<account_uid>;<account_name>;;<account_organization_uid>;ProwlerPro.onmicrosoft.com;;<finding_uid>;azure;aks_network_policy_enabled;Ensure Network Policy is Enabled and set as appropriate;;PASS;Network policy is enabled for cluster '<resource_name>' in subscription '<account_name>'.;False;aks;;medium;Microsoft.ContainerService/managedClusters;/subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name>;<resource_name>;;;<partition>;<region>;When you run modern, microservices-based applications in Kubernetes, you often want to control which components can communicate with each other. The principle of least privilege should be applied to how traffic can flow between pods in an Azure Kubernetes Service (AKS) cluster. Let's say you likely want to block traffic directly to back-end applications. The Network Policy feature in Kubernetes lets you define rules for ingress and egress traffic between pods in a cluster.;All pods in an AKS cluster can send and receive traffic without limitations, by default. To improve security, you can define rules that control the flow of traffic. Back-end applications are often only exposed to required front-end services, for example. Or, database components are only accessible to the application tiers that connect to them. Network Policy is a Kubernetes specification that defines access policies for communication between Pods. Using Network Policies, you define an ordered set of rules to send and receive traffic and apply them to a collection of pods that match one or more label selectors. These network policy rules are defined as YAML manifests. Network policies can be included as part of a wider manifest that also creates a deployment or service.;https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-network-security#ns-2-connect-private-networks-together;;https://learn.microsoft.com/en-us/azure/aks/use-network-policies;;https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_4#terraform;;;ENS-RD2022: mp.com.4.r2.az.aks.1;;;;Network Policy requires the Network Policy add-on. This add-on is included automatically when a cluster with Network Policy is created, but for an existing cluster, needs to be added prior to enabling Network Policy. Enabling/Disabling Network Policy causes a rolling update of all cluster nodes, similar to performing a cluster upgrade. This operation is long-running and will block other operations on the cluster (including delete) until it has run to completion. If Network Policy is used, a cluster must have at least 2 nodes of type n1-standard-1 or higher. The recommended minimum size cluster to run Network Policy enforcement is 3 n1-standard-1 instances. Enabling Network Policy enforcement consumes additional resources in nodes. Specifically, it increases the memory footprint of the kube-system process by approximately 128MB, and requires approximately 300 millicores of CPU.;<prowler_version>
1 AUTH_METHOD TIMESTAMP ACCOUNT_UID ACCOUNT_NAME ACCOUNT_EMAIL ACCOUNT_ORGANIZATION_UID ACCOUNT_ORGANIZATION_NAME ACCOUNT_TAGS FINDING_UID PROVIDER CHECK_ID CHECK_TITLE CHECK_TYPE STATUS STATUS_EXTENDED MUTED SERVICE_NAME SUBSERVICE_NAME SEVERITY RESOURCE_TYPE RESOURCE_UID RESOURCE_NAME RESOURCE_DETAILS RESOURCE_TAGS PARTITION REGION DESCRIPTION RISK RELATED_URL REMEDIATION_RECOMMENDATION_TEXT REMEDIATION_RECOMMENDATION_URL REMEDIATION_CODE_NATIVEIAC REMEDIATION_CODE_TERRAFORM REMEDIATION_CODE_CLI REMEDIATION_CODE_OTHER COMPLIANCE CATEGORIES DEPENDS_ON RELATED_TO NOTES PROWLER_VERSION
2 <auth_method> 2025-02-14 14:27:30.710664 <account_uid> <account_name> <account_organization_uid> ProwlerPro.onmicrosoft.com <finding_uid> azure aks_cluster_rbac_enabled Ensure AKS RBAC is enabled PASS RBAC is enabled for cluster '<resource_name>' in subscription '<account_name>'. False aks medium Microsoft.ContainerService/ManagedClusters /subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name> <resource_name> <partition> <region> Azure Kubernetes Service (AKS) can be configured to use Azure Active Directory (AD) for user authentication. In this configuration, you sign in to an AKS cluster using an Azure AD authentication token. You can also configure Kubernetes role-based access control (Kubernetes RBAC) to limit access to cluster resources based a user's identity or group membership. Kubernetes RBAC and AKS help you secure your cluster access and provide only the minimum required permissions to developers and operators. https://learn.microsoft.com/en-us/azure/aks/azure-ad-rbac?tabs=portal https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-privileged-access#pa-7-follow-just-enough-administration-least-privilege-principle https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_2#terraform https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/AKS/enable-role-based-access-control-for-kubernetes-service.html# ENS-RD2022: op.acc.2.az.r1.eid.1 <prowler_version>
3 <auth_method> 2025-02-14 14:27:30.710664 <account_uid> <account_name> <account_organization_uid> ProwlerPro.onmicrosoft.com <finding_uid> azure aks_clusters_created_with_private_nodes Ensure clusters are created with Private Nodes PASS Cluster '<resource_name>' was created with private nodes in subscription '<account_name>' False aks high Microsoft.ContainerService/ManagedClusters /subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name> <resource_name> <partition> <region> Disable public IP addresses for cluster nodes, so that they only have private IP addresses. Private Nodes are nodes with no public IP addresses. Disabling public IP addresses on cluster nodes restricts access to only internal networks, forcing attackers to obtain local network access before attempting to compromise the underlying Kubernetes hosts. https://learn.microsoft.com/en-us/azure/aks/private-clusters https://learn.microsoft.com/en-us/azure/aks/access-private-cluster ENS-RD2022: mp.com.4.r2.az.aks.1 | MITRE-ATTACK: T1190, T1530 <prowler_version>
4 <auth_method> 2025-02-14 14:27:30.710664 <account_uid> <account_name> <account_organization_uid> ProwlerPro.onmicrosoft.com <finding_uid> azure aks_clusters_public_access_disabled Ensure clusters are created with Private Endpoint Enabled and Public Access Disabled FAIL Public access to nodes is enabled for cluster '<resource_name>' in subscription '<account_name>' False aks high Microsoft.ContainerService/ManagedClusters /subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name> <resource_name> <partition> <region> Disable access to the Kubernetes API from outside the node network if it is not required. In a private cluster, the master node has two endpoints, a private and public endpoint. The private endpoint is the internal IP address of the master, behind an internal load balancer in the master's wirtual network. Nodes communicate with the master using the private endpoint. The public endpoint enables the Kubernetes API to be accessed from outside the master's virtual network. Although Kubernetes API requires an authorized token to perform sensitive actions, a vulnerability could potentially expose the Kubernetes publically with unrestricted access. Additionally, an attacker may be able to identify the current cluster and Kubernetes API version and determine whether it is vulnerable to an attack. Unless required, disabling public endpoint will help prevent such threats, and require the attacker to be on the master's virtual network to perform any attack on the Kubernetes API. https://learn.microsoft.com/en-us/azure/aks/private-clusters?tabs=azure-portal To use a private endpoint, create a new private endpoint in your virtual network then create a link between your virtual network and a new private DNS zone https://learn.microsoft.com/en-us/azure/aks/access-private-cluster?tabs=azure-cli az aks update -n <cluster_name> -g <resource_group> --disable-public-fqdn ENS-RD2022: mp.com.4.az.aks.2 | MITRE-ATTACK: T1190, T1530 <prowler_version>
5 <auth_method> 2025-02-14 14:27:30.710664 <account_uid> <account_name> <account_organization_uid> ProwlerPro.onmicrosoft.com <finding_uid> azure aks_network_policy_enabled Ensure Network Policy is Enabled and set as appropriate PASS Network policy is enabled for cluster '<resource_name>' in subscription '<account_name>'. False aks medium Microsoft.ContainerService/managedClusters /subscriptions/<account_uid>/resourcegroups/<resource_name>_group/providers/Microsoft.ContainerService/managedClusters/<resource_name> <resource_name> <partition> <region> When you run modern, microservices-based applications in Kubernetes, you often want to control which components can communicate with each other. The principle of least privilege should be applied to how traffic can flow between pods in an Azure Kubernetes Service (AKS) cluster. Let's say you likely want to block traffic directly to back-end applications. The Network Policy feature in Kubernetes lets you define rules for ingress and egress traffic between pods in a cluster. All pods in an AKS cluster can send and receive traffic without limitations, by default. To improve security, you can define rules that control the flow of traffic. Back-end applications are often only exposed to required front-end services, for example. Or, database components are only accessible to the application tiers that connect to them. Network Policy is a Kubernetes specification that defines access policies for communication between Pods. Using Network Policies, you define an ordered set of rules to send and receive traffic and apply them to a collection of pods that match one or more label selectors. These network policy rules are defined as YAML manifests. Network policies can be included as part of a wider manifest that also creates a deployment or service. https://learn.microsoft.com/en-us/security/benchmark/azure/security-controls-v2-network-security#ns-2-connect-private-networks-together https://learn.microsoft.com/en-us/azure/aks/use-network-policies https://docs.prowler.com/checks/azure/azure-kubernetes-policies/bc_azr_kubernetes_4#terraform ENS-RD2022: mp.com.4.r2.az.aks.1 Network Policy requires the Network Policy add-on. This add-on is included automatically when a cluster with Network Policy is created, but for an existing cluster, needs to be added prior to enabling Network Policy. Enabling/Disabling Network Policy causes a rolling update of all cluster nodes, similar to performing a cluster upgrade. This operation is long-running and will block other operations on the cluster (including delete) until it has run to completion. If Network Policy is used, a cluster must have at least 2 nodes of type n1-standard-1 or higher. The recommended minimum size cluster to run Network Policy enforcement is 3 n1-standard-1 instances. Enabling Network Policy enforcement consumes additional resources in nodes. Specifically, it increases the memory footprint of the kube-system process by approximately 128MB, and requires approximately 300 millicores of CPU. <prowler_version>
@@ -0,0 +1,552 @@
[
{
"message": "There are no AppInsight configured in subscription <subscription_name>.",
"metadata": {
"event_code": "appinsights_ensure_is_configured",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 2,
"severity": "Low",
"status": "New",
"status_code": "FAIL",
"status_detail": "There are no AppInsight configured in subscription <subscription_name>.",
"status_id": 1,
"unmapped": {
"related_url": "https://learn.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "Because Application Insights relies on a Log Analytics Workspace, an organization will incur additional expenses when using this service.",
"compliance": {
"CIS-2.1": [
"5.3.1"
],
"ENS-RD2022": [
"mp.s.4.r1.az.nt.2"
],
"CIS-3.0": [
"6.3.1"
],
"CIS-2.0": [
"5.3.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539650,
"created_time_dt": "2025-02-14T14:27:30.710664",
"desc": "Application Insights within Azure act as an Application Performance Monitoring solution providing valuable data into how well an application performs and additional information when performing incident response. The types of log data collected include application metrics, telemetry data, and application trace logging data providing organizations with detailed information about application activity and application transactions. Both data sets help organizations adopt a proactive and retroactive means to handle security and performance related metrics within their modern applications.",
"product_uid": "prowler",
"title": "Ensure Application Insights are Configured.",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "AzureCloud",
"region": "global",
"data": {
"details": "",
"metadata": {}
},
"group": {
"name": "appinsights"
},
"labels": [],
"name": "AppInsights",
"type": "Microsoft.Insights/components",
"uid": "AppInsights"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<subscription_name>",
"type": "Azure AD Account",
"type_id": 6,
"uid": "<subscription_uid>",
"labels": []
},
"org": {
"name": "<organization_name>",
"uid": "<tenant_uid>"
},
"provider": "azure",
"region": "global"
},
"remediation": {
"desc": "1. Navigate to Application Insights 2. Under the Basics tab within the PROJECT DETAILS section, select the Subscription 3. Select the Resource group 4. Within the INSTANCE DETAILS, enter a Name 5. Select a Region 6. Next to Resource Mode, select Workspace-based 7. Within the WORKSPACE DETAILS, select the Subscription for the log analytics workspace 8. Select the appropriate Log Analytics Workspace 9. Click Next:Tags > 10. Enter the appropriate Tags as Name, Value pairs. 11. Click Next:Review+Create 12. Click Create.",
"references": [
"az monitor app-insights component create --app <app name> --resource-group <resource group name> --location <location> --kind 'web' --retention-time <INT days to retain logs> --workspace <log analytics workspace ID> -- subscription <subscription ID>",
"https://www.tenable.com/audits/items/CIS_Microsoft_Azure_Foundations_v2.0.0_L2.audit:8a7a608d180042689ad9d3f16aa359f1"
]
},
"risk_details": "Configuring Application Insights provides additional data not found elsewhere within Azure as part of a much larger logging and monitoring program within an organization's Information Security practice. The types and contents of these logs will act as both a potential cost saving measure (application performance) and a means to potentially confirm the source of a potential incident (trace logging). Metrics and Telemetry data provide organizations with a proactive approach to cost savings by monitoring an application's performance, while the trace logging data provides necessary details in a reactive incident response scenario by helping organizations identify the potential source of an incident within their application.",
"time": 1739539650,
"time_dt": "2025-02-14T14:27:30.710664",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "There is not another correct email configured for subscription <subscription_name>.",
"metadata": {
"event_code": "defender_additional_email_configured_with_a_security_contact",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "There is not another correct email configured for subscription <subscription_name>.",
"status_id": 1,
"unmapped": {
"related_url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-provide-security-contact-details",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-2.1": [
"2.1.18"
],
"ENS-RD2022": [
"op.mon.3.r3.az.de.1"
],
"CIS-3.0": [
"3.1.13"
],
"CIS-2.0": [
"2.1.19"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539650,
"created_time_dt": "2025-02-14T14:27:30.710664",
"desc": "Microsoft Defender for Cloud emails the subscription owners whenever a high-severity alert is triggered for their subscription. You should provide a security contact email address as an additional email address.",
"product_uid": "prowler",
"title": "Ensure 'Additional email addresses' is Configured with a Security Contact Email",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "AzureCloud",
"region": "global",
"data": {
"details": "",
"metadata": {
"resource_id": "<resource_uid>",
"name": "<resource_name>",
"emails": "",
"phone": "",
"alert_notifications_minimal_severity": "High",
"alert_notifications_state": "On",
"notified_roles": [
"Owner"
],
"notified_roles_state": "On"
}
},
"group": {
"name": "defender"
},
"labels": [],
"name": "<resource_name>",
"type": "AzureEmailNotifications",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<subscription_name>",
"type": "Azure AD Account",
"type_id": 6,
"uid": "<subscription_uid>",
"labels": []
},
"org": {
"name": "<organization_name>",
"uid": "<tenant_uid>"
},
"provider": "azure",
"region": "global"
},
"remediation": {
"desc": "1. From Azure Home select the Portal Menu 2. Select Microsoft Defender for Cloud 3. Click on Environment Settings 4. Click on the appropriate Management Group, Subscription, or Workspace 5. Click on Email notifications 6. Enter a valid security contact email address (or multiple addresses separated by commas) in the Additional email addresses field 7. Click Save",
"references": [
"https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-security-contact-emails-is-set#terraform",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/security-contact-email.html",
"https://learn.microsoft.com/en-us/rest/api/defenderforcloud/security-contacts/list?view=rest-defenderforcloud-2020-01-01-preview&tabs=HTTP"
]
},
"risk_details": "Microsoft Defender for Cloud emails the Subscription Owner to notify them about security alerts. Adding your Security Contact's email address to the 'Additional email addresses' field ensures that your organization's Security Team is included in these alerts. This ensures that the proper people are aware of any potential compromise in order to mitigate the risk in a timely fashion.",
"time": 1739539650,
"time_dt": "2025-02-14T14:27:30.710664",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Defender Auto Provisioning Log Analytics Agents from subscription <subscription_name> is set to OFF.",
"metadata": {
"event_code": "defender_auto_provisioning_log_analytics_agent_vms_on",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "Defender Auto Provisioning Log Analytics Agents from subscription <subscription_name> is set to OFF.",
"status_id": 1,
"unmapped": {
"related_url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-data-security",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-2.1": [
"2.1.14"
],
"ENS-RD2022": [
"op.mon.3.r2.az.de.1",
"mp.s.4.r1.az.nt.5"
],
"MITRE-ATTACK": [
"T1190"
],
"CIS-3.0": [
"3.1.1.1"
],
"CIS-2.0": [
"2.1.15"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539650,
"created_time_dt": "2025-02-14T14:27:30.710664",
"desc": "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'. The Microsoft Monitoring Agent scans for various security-related configurations and events such as system updates, OS vulnerabilities, endpoint protection, and provides alerts.",
"product_uid": "prowler",
"title": "Ensure that Auto provisioning of 'Log Analytics agent for Azure VMs' is Set to 'On'",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "AzureCloud",
"region": "global",
"data": {
"details": "",
"metadata": {
"resource_id": "<resource_uid>",
"resource_name": "<resource_name>",
"resource_type": "Microsoft.Security/autoProvisioningSettings",
"auto_provision": "Off"
}
},
"group": {
"name": "defender"
},
"labels": [],
"name": "<resource_name>",
"type": "AzureDefenderPlan",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<subscription_name>",
"type": "Azure AD Account",
"type_id": 6,
"uid": "<subscription_uid>",
"labels": []
},
"org": {
"name": "<organization_name>",
"uid": "<tenant_uid>"
},
"provider": "azure",
"region": "global"
},
"remediation": {
"desc": "Ensure comprehensive visibility into possible security vulnerabilities, including missing updates, misconfigured operating system security settings, and active threats, allowing for timely mitigation and improved overall security posture",
"references": [
"https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/azure/SecurityCenter/automatic-provisioning-of-monitoring-agent.html",
"https://learn.microsoft.com/en-us/azure/defender-for-cloud/monitoring-components"
]
},
"risk_details": "Missing critical security information about your Azure VMs, such as security alerts, security recommendations, and change tracking.",
"time": 1739539650,
"time_dt": "2025-02-14T14:27:30.710664",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Container image scan is disabled in subscription <subscription_name>.",
"metadata": {
"event_code": "defender_container_images_scan_enabled",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "Container image scan is disabled in subscription <subscription_name>.",
"status_id": 1,
"unmapped": {
"related_url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-check-health",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "When using an Azure container registry, you might occasionally encounter problems. For example, you might not be able to pull a container image because of an issue with Docker in your local environment. Or, a network issue might prevent you from connecting to the registry.",
"compliance": {
"MITRE-ATTACK": [
"T1190",
"T1525"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539650,
"created_time_dt": "2025-02-14T14:27:30.710664",
"desc": "Scan images being deployed to Azure (AKS) for vulnerabilities. Vulnerability scanning for images stored in Azure Container Registry is generally available in Azure Security Center. This capability is powered by Qualys, a leading provider of information security. When you push an image to Container Registry, Security Center automatically scans it, then checks for known vulnerabilities in packages or dependencies defined in the file. When the scan completes (after about 10 minutes), Security Center provides details and a security classification for each vulnerability detected, along with guidance on how to remediate issues and protect vulnerable attack surfaces.",
"product_uid": "prowler",
"title": "Ensure Image Vulnerability Scanning using Azure Defender image scanning or a third party provider",
"types": [],
"uid": "prowler-azure-defender_container_images_scan_enabled-<subscription_uid>-global-Dender plan for Containers"
},
"resources": [
{
"cloud_partition": "AzureCloud",
"region": "global",
"data": {
"details": "",
"metadata": {
"resource_id": "<resource_uid>",
"pricing_tier": "Free",
"free_trial_remaining_time": 2592000.0,
"extensions": {}
}
},
"group": {
"name": "defender"
},
"labels": [],
"name": "<resource_name>",
"type": "Microsoft.Security",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<subscription_name>",
"type": "Azure AD Account",
"type_id": 6,
"uid": "<subscription_uid>",
"labels": []
},
"org": {
"name": "<organization_name>",
"uid": "<tenant_uid>"
},
"provider": "azure",
"region": "global"
},
"remediation": {
"desc": "",
"references": [
"https://learn.microsoft.com/en-us/azure/container-registry/scan-images-defender"
]
},
"risk_details": "Vulnerabilities in software packages can be exploited by hackers or malicious users to obtain unauthorized access to local cloud resources. Azure Defender and other third party products allow images to be scanned for known vulnerabilities.",
"time": 1739539650,
"time_dt": "2025-02-14T14:27:30.710664",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Defender plan Defender for App Services from subscription <subscription_name> is set to OFF (pricing tier not standard).",
"metadata": {
"event_code": "defender_ensure_defender_for_app_services_is_on",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 4,
"severity": "High",
"status": "New",
"status_code": "FAIL",
"status_detail": "Defender plan Defender for App Services from subscription <subscription_name> is set to OFF (pricing tier not standard).",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"CIS-2.1": [
"2.1.2"
],
"ENS-RD2022": [
"mp.s.4.r1.az.nt.3"
],
"MITRE-ATTACK": [
"T1190",
"T1059",
"T1204",
"T1552",
"T1486",
"T1499",
"T1496",
"T1087"
],
"CIS-3.0": [
"3.1.6.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539650,
"created_time_dt": "2025-02-14T14:27:30.710664",
"desc": "Ensure That Microsoft Defender for App Services Is Set To 'On' ",
"product_uid": "prowler",
"title": "Ensure That Microsoft Defender for App Services Is Set To 'On' ",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"cloud_partition": "AzureCloud",
"region": "global",
"data": {
"details": "",
"metadata": {
"resource_id": "<resource_uid>",
"pricing_tier": "Free",
"free_trial_remaining_time": 2592000.0,
"extensions": {}
}
},
"group": {
"name": "defender"
},
"labels": [],
"name": "<resource_name>",
"type": "AzureDefenderPlan",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<subscription_name>",
"type": "Azure AD Account",
"type_id": 6,
"uid": "<subscription_uid>",
"labels": []
},
"org": {
"name": "<organization_name>",
"uid": "<tenant_uid>"
},
"provider": "azure",
"region": "global"
},
"remediation": {
"desc": "By <resource_name>, Microsoft Defender for Cloud is not enabled for your App Service instances. Enabling the Defender security service for App Service instances allows for advanced security defense using threat detection capabilities provided by Microsoft Security Response Center.",
"references": [
"https://docs.prowler.com/checks/azure/azure-general-policies/ensure-that-azure-defender-is-set-to-on-for-app-service#terraform",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/defender-app-service.html",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/SecurityCenter/defender-app-service.html"
]
},
"risk_details": "Turning on Microsoft Defender for App Service enables threat detection for App Service, providing threat intelligence, anomaly detection, and behavior analytics in the Microsoft Defender for Cloud.",
"time": 1739539650,
"time_dt": "2025-02-14T14:27:30.710664",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
}
]
+5
View File
@@ -0,0 +1,5 @@
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;apikeys_key_exists;Ensure API Keys Only Exist for Active Services;;PASS;Project <account_uid> does not have active API Keys.;False;apikeys;;medium;API Key;<account_uid>;<account_name>;;;;<region>;API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead.;Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key.;;To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead.;https://cloud.google.com/docs/authentication/api-keys;;;gcloud alpha services api-keys delete;;MITRE-ATTACK: T1098 | CIS-2.0: 1.12 | ENS-RD2022: op.acc.2.gcp.rbak.1 | CIS-3.0: 1.12;;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;artifacts_container_analysis_enabled;Ensure Image Vulnerability Analysis using AR Container Analysis or a third-party provider;Security | Configuration;FAIL;AR Container Analysis is not enabled in project <account_uid>.;False;artifacts;Container Analysis;medium;Service;<resource_uid>;<resource_name>;;;;<region>;Scan images stored in Google Container Registry (GCR) for vulnerabilities using AR Container Analysis or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.;Without image vulnerability scanning, container images stored in Artifact Registry may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.;https://cloud.google.com/artifact-analysis/docs;Enable vulnerability scanning for images stored in Artifact Registry using AR Container Analysis or a third-party provider.;https://cloud.google.com/artifact-analysis/docs/container-scanning-overview;;;gcloud services enable containeranalysis.googleapis.com;;MITRE-ATTACK: T1525 | ENS-RD2022: op.exp.4.r4.gcp.log.1, op.mon.3.gcp.scc.1;;;;By default, AR Container Analysis is disabled.;<prowler_version>
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;compute_firewall_rdp_access_from_the_internet_allowed;Ensure That RDP Access Is Restricted From the Internet;;PASS;Firewall <resource_name> does not expose port 3389 (RDP) to the internet.;False;networking;;critical;FirewallRule;<resource_uid>;<resource_name>;;;;<region>;GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.;Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.;;Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.;https://cloud.google.com/vpc/docs/using-firewalls;;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command;https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html;MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7;internet-exposed;;;;<prowler_version>
<auth_method>;2025-02-14 14:27:20.697446;<account_uid>;<account_name>;;<account_organization_uid>;<account_organization_name>;<account_tags>;<finding_uid>;gcp;compute_firewall_rdp_access_from_the_internet_allowed;Ensure That RDP Access Is Restricted From the Internet;;PASS;Firewall <resource_name> does not expose port 3389 (RDP) to the internet.;False;networking;;critical;FirewallRule;<resource_uid>;<resource_name>;;;;<region>;GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.;Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.;;Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.;https://cloud.google.com/vpc/docs/using-firewalls;;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform;https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command;https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html;MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7;internet-exposed;;;;<prowler_version>
1 AUTH_METHOD TIMESTAMP ACCOUNT_UID ACCOUNT_NAME ACCOUNT_EMAIL ACCOUNT_ORGANIZATION_UID ACCOUNT_ORGANIZATION_NAME ACCOUNT_TAGS FINDING_UID PROVIDER CHECK_ID CHECK_TITLE CHECK_TYPE STATUS STATUS_EXTENDED MUTED SERVICE_NAME SUBSERVICE_NAME SEVERITY RESOURCE_TYPE RESOURCE_UID RESOURCE_NAME RESOURCE_DETAILS RESOURCE_TAGS PARTITION REGION DESCRIPTION RISK RELATED_URL REMEDIATION_RECOMMENDATION_TEXT REMEDIATION_RECOMMENDATION_URL REMEDIATION_CODE_NATIVEIAC REMEDIATION_CODE_TERRAFORM REMEDIATION_CODE_CLI REMEDIATION_CODE_OTHER COMPLIANCE CATEGORIES DEPENDS_ON RELATED_TO NOTES PROWLER_VERSION
2 <auth_method> 2025-02-14 14:27:20.697446 <account_uid> <account_name> <account_organization_uid> <account_organization_name> <account_tags> <finding_uid> gcp apikeys_key_exists Ensure API Keys Only Exist for Active Services PASS Project <account_uid> does not have active API Keys. False apikeys medium API Key <account_uid> <account_name> <region> API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead. Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key. To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead. https://cloud.google.com/docs/authentication/api-keys gcloud alpha services api-keys delete MITRE-ATTACK: T1098 | CIS-2.0: 1.12 | ENS-RD2022: op.acc.2.gcp.rbak.1 | CIS-3.0: 1.12 <prowler_version>
3 <auth_method> 2025-02-14 14:27:20.697446 <account_uid> <account_name> <account_organization_uid> <account_organization_name> <account_tags> <finding_uid> gcp artifacts_container_analysis_enabled Ensure Image Vulnerability Analysis using AR Container Analysis or a third-party provider Security | Configuration FAIL AR Container Analysis is not enabled in project <account_uid>. False artifacts Container Analysis medium Service <resource_uid> <resource_name> <region> Scan images stored in Google Container Registry (GCR) for vulnerabilities using AR Container Analysis or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images. Without image vulnerability scanning, container images stored in Artifact Registry may contain known vulnerabilities, increasing the risk of exploitation by malicious actors. https://cloud.google.com/artifact-analysis/docs Enable vulnerability scanning for images stored in Artifact Registry using AR Container Analysis or a third-party provider. https://cloud.google.com/artifact-analysis/docs/container-scanning-overview gcloud services enable containeranalysis.googleapis.com MITRE-ATTACK: T1525 | ENS-RD2022: op.exp.4.r4.gcp.log.1, op.mon.3.gcp.scc.1 By default, AR Container Analysis is disabled. <prowler_version>
4 <auth_method> 2025-02-14 14:27:20.697446 <account_uid> <account_name> <account_organization_uid> <account_organization_name> <account_tags> <finding_uid> gcp compute_firewall_rdp_access_from_the_internet_allowed Ensure That RDP Access Is Restricted From the Internet PASS Firewall <resource_name> does not expose port 3389 (RDP) to the internet. False networking critical FirewallRule <resource_uid> <resource_name> <region> GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided. Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks. Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server. https://cloud.google.com/vpc/docs/using-firewalls https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7 internet-exposed <prowler_version>
5 <auth_method> 2025-02-14 14:27:20.697446 <account_uid> <account_name> <account_organization_uid> <account_organization_name> <account_tags> <finding_uid> gcp compute_firewall_rdp_access_from_the_internet_allowed Ensure That RDP Access Is Restricted From the Internet PASS Firewall <resource_name> does not expose port 3389 (RDP) to the internet. False networking critical FirewallRule <resource_uid> <resource_name> <region> GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided. Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks. Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server. https://cloud.google.com/vpc/docs/using-firewalls https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform https://docs.<account_organization_name>/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html MITRE-ATTACK: T1190, T1199, T1048, T1498, T1046 | CIS-2.0: 3.7 | ENS-RD2022: mp.com.1.gcp.fw.1 | CIS-3.0: 3.7 internet-exposed <prowler_version>
@@ -0,0 +1,636 @@
[
{
"message": "Project <project_id> does not have active API Keys.",
"metadata": {
"event_code": "apikeys_key_exists",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "PASS",
"status_detail": "Project <project_id> does not have active API Keys.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"MITRE-ATTACK": [
"T1098"
],
"CIS-2.0": [
"1.12"
],
"ENS-RD2022": [
"op.acc.2.gcp.rbak.1"
],
"CIS-3.0": [
"1.12"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539640,
"created_time_dt": "2025-02-14T14:27:20.697446",
"desc": "API Keys should only be used for services in cases where other authentication methods are unavailable. Unused keys with their permissions in tact may still exist within a project. Keys are insecure because they can be viewed publicly, such as from within a browser, or they can be accessed on a device where the key resides. It is recommended to use standard authentication flow instead.",
"product_uid": "prowler",
"title": "Ensure API Keys Only Exist for Active Services",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"region": "global",
"data": {
"details": "",
"metadata": {
"number": "<uid>",
"id": "<project_id>",
"name": "<project_name>",
"organization": {
"id": "<tenant_uid>",
"name": "organizations/<tenant_uid>",
"display_name": "prowler.com"
},
"labels": {
"tag": "test",
"tag2": "test2",
"generative-language": "enabled"
},
"lifecycle_state": "ACTIVE"
}
},
"group": {
"name": "apikeys"
},
"labels": [],
"name": "<project_name>",
"type": "API Key",
"uid": "<project_id>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<project_name>",
"type": "GCP Account",
"type_id": 5,
"uid": "<project_id>",
"labels": [
"tag:test"
]
},
"org": {
"name": "prowler.com",
"uid": "<tenant_uid>"
},
"provider": "gcp",
"region": "global"
},
"remediation": {
"desc": "To avoid the security risk in using API keys, it is recommended to use standard authentication flow instead.",
"references": [
"gcloud alpha services api-keys delete",
"https://cloud.google.com/docs/authentication/api-keys"
]
},
"risk_details": "Security risks involved in using API-Keys appear below: API keys are simple encrypted strings, API keys do not identify the user or the application making the API request, API keys are typically accessible to clients, making it easy to discover and steal an API key.",
"time": 1739539640,
"time_dt": "2025-02-14T14:27:20.697446",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "AR Container Analysis is not enabled in project <project_id>.",
"metadata": {
"event_code": "artifacts_container_analysis_enabled",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "AR Container Analysis is not enabled in project <project_id>.",
"status_id": 1,
"unmapped": {
"related_url": "https://cloud.google.com/artifact-analysis/docs",
"categories": [],
"depends_on": [],
"related_to": [],
"notes": "By default, AR Container Analysis is disabled.",
"compliance": {
"MITRE-ATTACK": [
"T1525"
],
"ENS-RD2022": [
"op.exp.4.r4.gcp.log.1",
"op.mon.3.gcp.scc.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539640,
"created_time_dt": "2025-02-14T14:27:20.697446",
"desc": "Scan images stored in Google Container Registry (GCR) for vulnerabilities using AR Container Analysis or a third-party provider. This helps identify and mitigate security risks associated with known vulnerabilities in container images.",
"product_uid": "prowler",
"title": "Ensure Image Vulnerability Analysis using AR Container Analysis or a third-party provider",
"types": [
"Security",
"Configuration"
],
"uid": "<finding_uid>"
},
"resources": [
{
"region": "global",
"data": {
"details": "",
"metadata": {
"number": "538174383574",
"id": "<project_id>",
"name": "<project_name>",
"organization": {
"id": "<tenant_uid>",
"name": "organizations/<tenant_uid>",
"display_name": "prowler.com"
},
"labels": {
"tag": "test",
"tag2": "test2",
"generative-language": "enabled"
},
"lifecycle_state": "ACTIVE"
}
},
"group": {
"name": "artifacts"
},
"labels": [],
"name": "AR Container Analysis",
"type": "Service",
"uid": "containeranalysis.googleapis.com"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<project_name>",
"type": "GCP Account",
"type_id": 5,
"uid": "<project_id>",
"labels": [
"tag:test"
]
},
"org": {
"name": "prowler.com",
"uid": "<tenant_uid>"
},
"provider": "gcp",
"region": "global"
},
"remediation": {
"desc": "Enable vulnerability scanning for images stored in Artifact Registry using AR Container Analysis or a third-party provider.",
"references": [
"gcloud services enable containeranalysis.googleapis.com",
"https://cloud.google.com/artifact-analysis/docs/container-scanning-overview"
]
},
"risk_details": "Without image vulnerability scanning, container images stored in Artifact Registry may contain known vulnerabilities, increasing the risk of exploitation by malicious actors.",
"time": 1739539640,
"time_dt": "2025-02-14T14:27:20.697446",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
"metadata": {
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 5,
"severity": "Critical",
"status": "New",
"status_code": "PASS",
"status_detail": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [
"internet-exposed"
],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"MITRE-ATTACK": [
"T1190",
"T1199",
"T1048",
"T1498",
"T1046"
],
"CIS-2.0": [
"3.7"
],
"ENS-RD2022": [
"mp.com.1.gcp.fw.1"
],
"CIS-3.0": [
"3.7"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539640,
"created_time_dt": "2025-02-14T14:27:20.697446",
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
"product_uid": "prowler",
"title": "Ensure That RDP Access Is Restricted From the Internet",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"region": "global",
"data": {
"details": "",
"metadata": {
"name": "<resource_id>",
"id": "<uid>",
"source_ranges": [
"<value>"
],
"direction": "INGRESS",
"allowed_rules": [
{
"IPProtocol": "icmp"
}
],
"project_id": "<project_id>"
}
},
"group": {
"name": "networking"
},
"labels": [],
"name": "<resource_id>",
"type": "FirewallRule",
"uid": "<uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<project_name>",
"type": "GCP Account",
"type_id": 5,
"uid": "<project_id>",
"labels": [
"tag:test",
"tag2:test2"
]
},
"org": {
"name": "prowler.com",
"uid": "<tenant_uid>"
},
"provider": "gcp",
"region": "global"
},
"remediation": {
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
"references": [
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
"https://cloud.google.com/vpc/docs/using-firewalls"
]
},
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
"time": 1739539640,
"time_dt": "2025-02-14T14:27:20.697446",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
"metadata": {
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 5,
"severity": "Critical",
"status": "New",
"status_code": "PASS",
"status_detail": "Firewall <resource_id> does not expose port 3389 (RDP) to the internet.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [
"internet-exposed"
],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"MITRE-ATTACK": [
"T1190",
"T1199",
"T1048",
"T1498",
"T1046"
],
"CIS-2.0": [
"3.7"
],
"ENS-RD2022": [
"mp.com.1.gcp.fw.1"
],
"CIS-3.0": [
"3.7"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539640,
"created_time_dt": "2025-02-14T14:27:20.697446",
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
"product_uid": "prowler",
"title": "Ensure That RDP Access Is Restricted From the Internet",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"region": "global",
"data": {
"details": "",
"metadata": {
"name": "<resource_id>",
"id": "<uid>",
"source_ranges": [
"<value>"
],
"direction": "INGRESS",
"allowed_rules": [
{
"IPProtocol": "tcp",
"ports": [
"0-65535"
]
},
{
"IPProtocol": "udp",
"ports": [
"0-65535"
]
},
{
"IPProtocol": "icmp"
}
],
"project_id": "<project_id>"
}
},
"group": {
"name": "networking"
},
"labels": [],
"name": "<resource_id>",
"type": "FirewallRule",
"uid": "<uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<project_name>",
"type": "GCP Account",
"type_id": 5,
"uid": "<project_id>",
"labels": [
"tag:test",
"tag2:test2"
]
},
"org": {
"name": "prowler.com",
"uid": "<tenant_uid>"
},
"provider": "gcp",
"region": "global"
},
"remediation": {
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
"references": [
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
"https://cloud.google.com/vpc/docs/using-firewalls"
]
},
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
"time": 1739539640,
"time_dt": "2025-02-14T14:27:20.697446",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Firewall <resource_id> does exposes port 3389 (RDP) to the internet.",
"metadata": {
"event_code": "compute_firewall_rdp_access_from_the_internet_allowed",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"cloud",
"datetime"
],
"tenant_uid": "<tenant_uid>",
"version": "1.4.0"
},
"severity_id": 5,
"severity": "Critical",
"status": "New",
"status_code": "FAIL",
"status_detail": "Firewall <resource_id> does exposes port 3389 (RDP) to the internet.",
"status_id": 1,
"unmapped": {
"related_url": "",
"categories": [
"internet-exposed"
],
"depends_on": [],
"related_to": [],
"notes": "",
"compliance": {
"MITRE-ATTACK": [
"T1190",
"T1199",
"T1048",
"T1498",
"T1046"
],
"CIS-2.0": [
"3.7"
],
"ENS-RD2022": [
"mp.com.1.gcp.fw.1"
],
"CIS-3.0": [
"3.7"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539640,
"created_time_dt": "2025-02-14T14:27:20.697446",
"desc": "GCP `Firewall Rules` are specific to a `VPC Network`. Each rule either `allows` or `denies` traffic when its conditions are met. Its conditions allow users to specify the type of traffic, such as ports and protocols, and the source or destination of the traffic, including IP addresses, subnets, and instances. Firewall rules are defined at the VPC network level and are specific to the network in which they are defined. The rules themselves cannot be shared among networks. Firewall rules only support IPv4 traffic. When specifying a source for an ingress rule or a destination for an egress rule by address, an `IPv4` address or `IPv4 block in CIDR` notation can be used. Generic `(0.0.0.0/0)` incoming traffic from the Internet to a VPC or VM instance using `RDP` on `Port 3389` can be avoided.",
"product_uid": "prowler",
"title": "Ensure That RDP Access Is Restricted From the Internet",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"region": "global",
"data": {
"details": "",
"metadata": {
"name": "<resource_id>",
"id": "<uid>",
"source_ranges": [
"<value>"
],
"direction": "INGRESS",
"allowed_rules": [
{
"IPProtocol": "tcp",
"ports": [
"3389"
]
}
],
"project_id": "<project_id>"
}
},
"group": {
"name": "networking"
},
"labels": [],
"name": "<resource_id>",
"type": "FirewallRule",
"uid": "<uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"cloud": {
"account": {
"name": "<project_name>",
"type": "GCP Account",
"type_id": 5,
"uid": "<project_id>",
"labels": [
"tag:test",
"tag2:test2"
]
},
"org": {
"name": "prowler.com",
"uid": "<tenant_uid>"
},
"provider": "gcp",
"region": "global"
},
"remediation": {
"desc": "Ensure that Google Cloud Virtual Private Cloud (VPC) firewall rules do not allow unrestricted access (i.e. 0.0.0.0/0) on TCP port 3389 in order to restrict Remote Desktop Protocol (RDP) traffic to trusted IP addresses or IP ranges only and reduce the attack surface. TCP port 3389 is used for secure remote GUI login to Windows VM instances by connecting a RDP client application with an RDP server.",
"references": [
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#terraform",
"https://docs.prowler.com/checks/gcp/google-cloud-networking-policies/bc_gcp_networking_2#cli-command",
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/CloudVPC/unrestricted-rdp-access.html",
"https://cloud.google.com/vpc/docs/using-firewalls"
]
},
"risk_details": "Allowing unrestricted Remote Desktop Protocol (RDP) access can increase opportunities for malicious activities such as hacking, Man-In-The-Middle attacks (MITM) and Pass-The-Hash (PTH) attacks.",
"time": 1739539640,
"time_dt": "2025-02-14T14:27:20.697446",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
}
]
@@ -0,0 +1,5 @@
AUTH_METHOD;TIMESTAMP;ACCOUNT_UID;ACCOUNT_NAME;ACCOUNT_EMAIL;ACCOUNT_ORGANIZATION_UID;ACCOUNT_ORGANIZATION_NAME;ACCOUNT_TAGS;FINDING_UID;PROVIDER;CHECK_ID;CHECK_TITLE;CHECK_TYPE;STATUS;STATUS_EXTENDED;MUTED;SERVICE_NAME;SUBSERVICE_NAME;SEVERITY;RESOURCE_TYPE;RESOURCE_UID;RESOURCE_NAME;RESOURCE_DETAILS;RESOURCE_TAGS;PARTITION;REGION;DESCRIPTION;RISK;RELATED_URL;REMEDIATION_RECOMMENDATION_TEXT;REMEDIATION_RECOMMENDATION_URL;REMEDIATION_CODE_NATIVEIAC;REMEDIATION_CODE_TERRAFORM;REMEDIATION_CODE_CLI;REMEDIATION_CODE_OTHER;COMPLIANCE;CATEGORIES;DEPENDS_ON;RELATED_TO;NOTES;PROWLER_VERSION
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_always_pull_images_plugin;Ensure that the admission control plugin AlwaysPullImages is set;;FAIL;AlwaysPullImages admission control plugin is not set in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check verifies that the AlwaysPullImages admission control plugin is enabled in the Kubernetes API server. This plugin ensures that every new pod always pulls the required images, enforcing image access control and preventing the use of possibly outdated or altered images.;Without AlwaysPullImages, once an image is pulled to a node, any pod can use it without any authorization check, potentially leading to security risks.;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages;Configure the API server to use the AlwaysPullImages admission control plugin to ensure image security and integrity.;https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-admission-control-plugin-alwayspullimages-is-set#kubernetes;;--enable-admission-plugins=...,AlwaysPullImages,...;;CIS-1.10: 1.2.11 | CIS-1.8: 1.2.11;cluster-security;;;Enabling AlwaysPullImages can increase network and registry load and decrease container startup speed. It may not be suitable for all environments.;<prowler_version>
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_anonymous_requests;Ensure that the --anonymous-auth argument is set to false;;PASS;API Server does not have anonymous-auth enabled in pod <resource_uid>;False;apiserver;;high;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;Disable anonymous requests to the API server. When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests, which are then served by the API server. Disallowing anonymous requests strengthens security by ensuring all access is authenticated.;Enabling anonymous access to the API server can expose the cluster to unauthorized access and potential security vulnerabilities.;https://kubernetes.io/docs/admin/authentication/#anonymous-requests;Ensure the --anonymous-auth argument in the API server is set to false. This will reject all anonymous requests, enforcing authenticated access to the server.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-anonymous-auth-argument-is-set-to-false-1#kubernetes;;--anonymous-auth=false;;CIS-1.10: 1.2.1 | CIS-1.8: 1.2.1;trustboundaries;;;While anonymous access can be useful for health checks and discovery, consider the security implications for your specific environment.;<prowler_version>
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_audit_log_maxage_set;Ensure that the --audit-log-maxage argument is set to 30 or as appropriate;;FAIL;Audit log max age is not set to 30 or as appropriate in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check ensures that the Kubernetes API server is configured with an appropriate audit log retention period. Setting --audit-log-maxage to 30 or as per business requirements helps in maintaining logs for sufficient time to investigate past events.;Without an adequate log retention period, there may be insufficient audit history to investigate and analyze past events or security incidents.;https://kubernetes.io/docs/concepts/cluster-administration/audit/;Configure the API server audit log retention period to retain logs for at least 30 days or as per your organization's requirements.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxage-argument-is-set-to-30-or-as-appropriate#kubernetes;;--audit-log-maxage=30;;CIS-1.10: 1.2.17 | CIS-1.8: 1.2.18;logging;;;Ensure the audit log retention period is set appropriately to balance between storage constraints and the need for historical data.;<prowler_version>
<auth_method>;2025-02-14 14:27:38.533897;<account_uid>;context: <context>;;;;;<finding_uid>;kubernetes;apiserver_audit_log_maxbackup_set;Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate;;FAIL;Audit log max backup is not set to 10 or as appropriate in pod <resource_uid>;False;apiserver;;medium;KubernetesAPIServer;<resource_id>;<resource_name>;;;;namespace: kube-system;This check ensures that the Kubernetes API server is configured with an appropriate number of audit log backups. Setting --audit-log-maxbackup to 10 or as per business requirements helps maintain a sufficient log backup for investigations or analysis.;Without an adequate number of audit log backups, there may be insufficient log history to investigate past events or security incidents.;https://kubernetes.io/docs/concepts/cluster-administration/audit/;Configure the API server audit log backup retention to 10 or as per your organization's requirements.;https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/;https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxbackup-argument-is-set-to-10-or-as-appropriate#kubernetes;;--audit-log-maxbackup=10;;CIS-1.10: 1.2.18 | CIS-1.8: 1.2.19;logging;;;Ensure the audit log backup retention period is set appropriately to balance between storage constraints and the need for historical data.;<prowler_version>
1 AUTH_METHOD TIMESTAMP ACCOUNT_UID ACCOUNT_NAME ACCOUNT_EMAIL ACCOUNT_ORGANIZATION_UID ACCOUNT_ORGANIZATION_NAME ACCOUNT_TAGS FINDING_UID PROVIDER CHECK_ID CHECK_TITLE CHECK_TYPE STATUS STATUS_EXTENDED MUTED SERVICE_NAME SUBSERVICE_NAME SEVERITY RESOURCE_TYPE RESOURCE_UID RESOURCE_NAME RESOURCE_DETAILS RESOURCE_TAGS PARTITION REGION DESCRIPTION RISK RELATED_URL REMEDIATION_RECOMMENDATION_TEXT REMEDIATION_RECOMMENDATION_URL REMEDIATION_CODE_NATIVEIAC REMEDIATION_CODE_TERRAFORM REMEDIATION_CODE_CLI REMEDIATION_CODE_OTHER COMPLIANCE CATEGORIES DEPENDS_ON RELATED_TO NOTES PROWLER_VERSION
2 <auth_method> 2025-02-14 14:27:38.533897 <account_uid> context: <context> <finding_uid> kubernetes apiserver_always_pull_images_plugin Ensure that the admission control plugin AlwaysPullImages is set FAIL AlwaysPullImages admission control plugin is not set in pod <resource_uid> False apiserver medium KubernetesAPIServer <resource_id> <resource_name> namespace: kube-system This check verifies that the AlwaysPullImages admission control plugin is enabled in the Kubernetes API server. This plugin ensures that every new pod always pulls the required images, enforcing image access control and preventing the use of possibly outdated or altered images. Without AlwaysPullImages, once an image is pulled to a node, any pod can use it without any authorization check, potentially leading to security risks. https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages Configure the API server to use the AlwaysPullImages admission control plugin to ensure image security and integrity. https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-admission-control-plugin-alwayspullimages-is-set#kubernetes --enable-admission-plugins=...,AlwaysPullImages,... CIS-1.10: 1.2.11 | CIS-1.8: 1.2.11 cluster-security Enabling AlwaysPullImages can increase network and registry load and decrease container startup speed. It may not be suitable for all environments. <prowler_version>
3 <auth_method> 2025-02-14 14:27:38.533897 <account_uid> context: <context> <finding_uid> kubernetes apiserver_anonymous_requests Ensure that the --anonymous-auth argument is set to false PASS API Server does not have anonymous-auth enabled in pod <resource_uid> False apiserver high KubernetesAPIServer <resource_id> <resource_name> namespace: kube-system Disable anonymous requests to the API server. When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests, which are then served by the API server. Disallowing anonymous requests strengthens security by ensuring all access is authenticated. Enabling anonymous access to the API server can expose the cluster to unauthorized access and potential security vulnerabilities. https://kubernetes.io/docs/admin/authentication/#anonymous-requests Ensure the --anonymous-auth argument in the API server is set to false. This will reject all anonymous requests, enforcing authenticated access to the server. https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-anonymous-auth-argument-is-set-to-false-1#kubernetes --anonymous-auth=false CIS-1.10: 1.2.1 | CIS-1.8: 1.2.1 trustboundaries While anonymous access can be useful for health checks and discovery, consider the security implications for your specific environment. <prowler_version>
4 <auth_method> 2025-02-14 14:27:38.533897 <account_uid> context: <context> <finding_uid> kubernetes apiserver_audit_log_maxage_set Ensure that the --audit-log-maxage argument is set to 30 or as appropriate FAIL Audit log max age is not set to 30 or as appropriate in pod <resource_uid> False apiserver medium KubernetesAPIServer <resource_id> <resource_name> namespace: kube-system This check ensures that the Kubernetes API server is configured with an appropriate audit log retention period. Setting --audit-log-maxage to 30 or as per business requirements helps in maintaining logs for sufficient time to investigate past events. Without an adequate log retention period, there may be insufficient audit history to investigate and analyze past events or security incidents. https://kubernetes.io/docs/concepts/cluster-administration/audit/ Configure the API server audit log retention period to retain logs for at least 30 days or as per your organization's requirements. https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxage-argument-is-set-to-30-or-as-appropriate#kubernetes --audit-log-maxage=30 CIS-1.10: 1.2.17 | CIS-1.8: 1.2.18 logging Ensure the audit log retention period is set appropriately to balance between storage constraints and the need for historical data. <prowler_version>
5 <auth_method> 2025-02-14 14:27:38.533897 <account_uid> context: <context> <finding_uid> kubernetes apiserver_audit_log_maxbackup_set Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate FAIL Audit log max backup is not set to 10 or as appropriate in pod <resource_uid> False apiserver medium KubernetesAPIServer <resource_id> <resource_name> namespace: kube-system This check ensures that the Kubernetes API server is configured with an appropriate number of audit log backups. Setting --audit-log-maxbackup to 10 or as per business requirements helps maintain a sufficient log backup for investigations or analysis. Without an adequate number of audit log backups, there may be insufficient log history to investigate past events or security incidents. https://kubernetes.io/docs/concepts/cluster-administration/audit/ Configure the API server audit log backup retention to 10 or as per your organization's requirements. https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxbackup-argument-is-set-to-10-or-as-appropriate#kubernetes --audit-log-maxbackup=10 CIS-1.10: 1.2.18 | CIS-1.8: 1.2.19 logging Ensure the audit log backup retention period is set appropriately to balance between storage constraints and the need for historical data. <prowler_version>
@@ -0,0 +1,800 @@
[
{
"message": "AlwaysPullImages admission control plugin is not set in pod <pod>.",
"metadata": {
"event_code": "apiserver_always_pull_images_plugin",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "AlwaysPullImages admission control plugin is not set in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#alwayspullimages",
"categories": [
"cluster-security"
],
"depends_on": [],
"related_to": [],
"notes": "Enabling AlwaysPullImages can increase network and registry load and decrease container startup speed. It may not be suitable for all environments.",
"compliance": {
"CIS-1.10": [
"1.2.11"
],
"CIS-1.8": [
"1.2.11"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "This check verifies that the AlwaysPullImages admission control plugin is enabled in the Kubernetes API server. This plugin ensures that every new pod always pulls the required images, enforcing image access control and preventing the use of possibly outdated or altered images.",
"product_uid": "prowler",
"title": "Ensure that the admission control plugin AlwaysPullImages is set",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Configure the API server to use the AlwaysPullImages admission control plugin to ensure image security and integrity.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-admission-control-plugin-alwayspullimages-is-set#kubernetes",
"--enable-admission-plugins=...,AlwaysPullImages,...",
"https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers"
]
},
"risk_details": "Without AlwaysPullImages, once an image is pulled to a node, any pod can use it without any authorization check, potentially leading to security risks.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "API Server does not have anonymous-auth enabled in pod <pod>.",
"metadata": {
"event_code": "apiserver_anonymous_requests",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 4,
"severity": "High",
"status": "New",
"status_code": "PASS",
"status_detail": "API Server does not have anonymous-auth enabled in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/admin/authentication/#anonymous-requests",
"categories": [
"trustboundaries"
],
"depends_on": [],
"related_to": [],
"notes": "While anonymous access can be useful for health checks and discovery, consider the security implications for your specific environment.",
"compliance": {
"CIS-1.10": [
"1.2.1"
],
"CIS-1.8": [
"1.2.1"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "Disable anonymous requests to the API server. When enabled, requests that are not rejected by other configured authentication methods are treated as anonymous requests, which are then served by the API server. Disallowing anonymous requests strengthens security by ensuring all access is authenticated.",
"product_uid": "prowler",
"title": "Ensure that the --anonymous-auth argument is set to false",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<resource_uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Ensure the --anonymous-auth argument in the API server is set to false. This will reject all anonymous requests, enforcing authenticated access to the server.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-anonymous-auth-argument-is-set-to-false-1#kubernetes",
"--anonymous-auth=false",
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
]
},
"risk_details": "Enabling anonymous access to the API server can expose the cluster to unauthorized access and potential security vulnerabilities.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Audit log max age is not set to 30 or as appropriate in pod <pod>.",
"metadata": {
"event_code": "apiserver_audit_log_maxage_set",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "Audit log max age is not set to 30 or as appropriate in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
"categories": [
"logging"
],
"depends_on": [],
"related_to": [],
"notes": "Ensure the audit log retention period is set appropriately to balance between storage constraints and the need for historical data.",
"compliance": {
"CIS-1.10": [
"1.2.17"
],
"CIS-1.8": [
"1.2.18"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate audit log retention period. Setting --audit-log-maxage to 30 or as per business requirements helps in maintaining logs for sufficient time to investigate past events.",
"product_uid": "prowler",
"title": "Ensure that the --audit-log-maxage argument is set to 30 or as appropriate",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<resource_uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Configure the API server audit log retention period to retain logs for at least 30 days or as per your organization's requirements.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxage-argument-is-set-to-30-or-as-appropriate#kubernetes",
"--audit-log-maxage=30",
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
]
},
"risk_details": "Without an adequate log retention period, there may be insufficient audit history to investigate and analyze past events or security incidents.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Audit log max backup is not set to 10 or as appropriate in pod <pod>.",
"metadata": {
"event_code": "apiserver_audit_log_maxbackup_set",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "Audit log max backup is not set to 10 or as appropriate in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
"categories": [
"logging"
],
"depends_on": [],
"related_to": [],
"notes": "Ensure the audit log backup retention period is set appropriately to balance between storage constraints and the need for historical data.",
"compliance": {
"CIS-1.10": [
"1.2.18"
],
"CIS-1.8": [
"1.2.19"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate number of audit log backups. Setting --audit-log-maxbackup to 10 or as per business requirements helps maintain a sufficient log backup for investigations or analysis.",
"product_uid": "prowler",
"title": "Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<resource_uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Configure the API server audit log backup retention to 10 or as per your organization's requirements.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxbackup-argument-is-set-to-10-or-as-appropriate#kubernetes",
"--audit-log-maxbackup=10",
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
]
},
"risk_details": "Without an adequate number of audit log backups, there may be insufficient log history to investigate past events or security incidents.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Audit log max size is not set to 100 MB or as appropriate in pod <pod>.",
"metadata": {
"event_code": "apiserver_audit_log_maxsize_set",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 3,
"severity": "Medium",
"status": "New",
"status_code": "FAIL",
"status_detail": "Audit log max size is not set to 100 MB or as appropriate in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
"categories": [
"logging"
],
"depends_on": [],
"related_to": [],
"notes": "Adjust the audit log file size limit based on your organization's storage capabilities and logging requirements.",
"compliance": {
"CIS-1.10": [
"1.2.19"
],
"CIS-1.8": [
"1.2.20"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "This check ensures that the Kubernetes API server is configured with an appropriate audit log file size limit. Setting --audit-log-maxsize to 100 MB or as per business requirements helps manage the size of log files and prevents them from growing excessively large.",
"product_uid": "prowler",
"title": "Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<resource_uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Configure the API server audit log file size limit to 100 MB or as per your organization's requirements.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-maxsize-argument-is-set-to-100-or-as-appropriate#kubernetes",
"--audit-log-maxsize=100",
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
]
},
"risk_details": "Without an appropriate audit log file size limit, log files can grow excessively large, potentially leading to storage issues and difficulty in log analysis.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
},
{
"message": "Audit log path is not set in pod <pod>.",
"metadata": {
"event_code": "apiserver_audit_log_path_set",
"product": {
"name": "Prowler",
"uid": "prowler",
"vendor_name": "Prowler",
"version": "5.4.0"
},
"profiles": [
"container",
"datetime"
],
"version": "1.4.0"
},
"severity_id": 4,
"severity": "High",
"status": "New",
"status_code": "FAIL",
"status_detail": "Audit log path is not set in pod <pod>.",
"status_id": 1,
"unmapped": {
"related_url": "https://kubernetes.io/docs/concepts/cluster-administration/audit/",
"categories": [
"logging"
],
"depends_on": [],
"related_to": [],
"notes": "Audit logs are not enabled by default in Kubernetes. Configuring them is essential for security monitoring and forensic analysis.",
"compliance": {
"CIS-1.10": [
"1.2.16"
],
"CIS-1.8": [
"1.2.17"
]
}
},
"activity_name": "Create",
"activity_id": 1,
"finding_info": {
"created_time": 1739539658,
"created_time_dt": "2025-02-14T14:27:38.533897",
"desc": "This check verifies that the Kubernetes API server is configured with an audit log path. Enabling audit logs helps in maintaining a chronological record of all activities and operations which can be critical for security analysis and troubleshooting.",
"product_uid": "prowler",
"title": "Ensure that the --audit-log-path argument is set",
"types": [],
"uid": "<finding_uid>"
},
"resources": [
{
"data": {
"details": "",
"metadata": {
"name": "<pod>",
"uid": "<resource_uid>",
"namespace": "<namespace>",
"labels": {
"component": "kube-apiserver",
"tier": "control-plane"
},
"annotations": {
"kubernetes.io/config.source": "file"
},
"node_name": "<node_name>",
"service_account": null,
"status_phase": "Running",
"pod_ip": "<ip>",
"host_ip": "<ip>",
"host_pid": null,
"host_ipc": null,
"host_network": "True",
"security_context": {
"app_armor_profile": null,
"fs_group": null,
"fs_group_change_policy": null,
"run_as_group": null,
"run_as_non_root": null,
"run_as_user": null,
"se_linux_change_policy": null,
"se_linux_options": null,
"seccomp_profile": {
"localhost_profile": null,
"type": "RuntimeDefault"
},
"supplemental_groups": null,
"supplemental_groups_policy": null,
"sysctls": null,
"windows_options": null
},
"containers": {
"kube-apiserver": {
"name": "kube-apiserver",
"image": "<image>",
"command": [
"<command>"
],
"ports": null,
"env": null,
"security_context": {}
}
}
}
},
"group": {
"name": "apiserver"
},
"labels": [],
"name": "<pod>",
"namespace": "<namespace>",
"type": "KubernetesAPIServer",
"uid": "<resource_uid>"
}
],
"category_name": "Findings",
"category_uid": 2,
"class_name": "Detection Finding",
"class_uid": 2004,
"remediation": {
"desc": "Enable audit logging in the API server by specifying a valid path for --audit-log-path to ensure comprehensive activity logging within the cluster.",
"references": [
"https://docs.prowler.com/checks/kubernetes/kubernetes-policy-index/ensure-that-the-audit-log-path-argument-is-set#kubernetes",
"--audit-log-path=/var/log/apiserver/audit.log",
"https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/"
]
},
"risk_details": "Without audit logs, it becomes difficult to track changes and activities within the cluster, potentially obscuring the detection of malicious activities or operational issues.",
"time": 1739539658,
"time_dt": "2025-02-14T14:27:38.533897",
"type_uid": 200401,
"type_name": "Detection Finding: Create"
}
]
+418
View File
@@ -0,0 +1,418 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup and Configure Logger\n",
"This section configures the Python logging system to filter Prowler's output messages during security scans. We set the logging level to `CRITICAL`."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"\n",
"logging.basicConfig(level=logging.CRITICAL)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Initialize AWS Provider"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the Prowler's provider you want to scan\n",
"from prowler.providers.aws.aws_provider import AwsProvider\n",
"import json\n",
"\n",
"# Path to credentials file\n",
"credentials_path = \"./secrets-sdk/credentials.json\"\n",
"\n",
"# Load credentials from JSON file\n",
"try:\n",
" with open(credentials_path, \"r\") as f:\n",
" aws_credentials = json.load(f)\n",
" print(\"AWS credentials loaded successfully from file\")\n",
"except (FileNotFoundError, json.JSONDecodeError):\n",
" print(\"Invalid or missing JSON credentials file\")\n",
" aws_credentials = {\n",
" \"aws_access_key_id\": \"YOUR_ACCESS_KEY\",\n",
" \"aws_secret_access_key\": \"YOUR_SECRET_KEY\",\n",
" \"aws_session_token\": \"YOUR_SESSION_TOKEN\"\n",
" }\n",
"\n",
"# Optional: Test the AWS provider credentials before instantiation to verify that credentials work\n",
"aws_connection = AwsProvider.test_connection(**aws_credentials)\n",
"print(f\"AWS Test Connection:\\n\\t- Connected: {aws_connection.is_connected}\\n\\t- Error (if any): {aws_connection.error}\\n\")\n",
"\n",
"# Initialize the AWS provider with static credentials\n",
"aws = AwsProvider(**aws_credentials)\n",
"\n",
"# AWS Identity Information\n",
"print(f\"AWS Identity Information:\\n\\t- Account Number: {aws.identity.account}\\n\\t- User ID: {aws.identity.user_id}\\n\")\n",
"\n",
"# Alternative Providers (commented out)\n",
"# from prowler.providers.gcp.gcp_provider import GcpProvider\n",
"# from prowler.providers.azure.azure_provider import AzureProvider\n",
"# from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider\n",
"# from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import pprint\n",
"pprint.pp(aws.identity)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Mutelist"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Mutelist\n",
"from prowler.providers.aws.lib.mutelist.mutelist import AWSMutelist\n",
"\n",
"\n",
"mutelist_content = {\n",
" \"Accounts\": {\n",
" \"*\": {\n",
" \"Checks\": {\n",
" \"s3_account_level_public_access_blocks\": {\n",
" \"Tags\": [\"*\"],\n",
" \"Regions\": [\"*\"],\n",
" \"Resources\": [\"*\"],\n",
" }\n",
" }\n",
" }\n",
" }\n",
"}\n",
"mutelist_object = AWSMutelist(\n",
" mutelist_content=mutelist_content,\n",
")\n",
"aws._mutelist = mutelist_object"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## List Available Security Checks\n",
"Explore different ways to list security checks by provider, service, severity, and category."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the CheckMetadata class to list security checks\n",
"from prowler.lib.check.models import CheckMetadata\n",
"\n",
"# List all available checks\n",
"checks = CheckMetadata.list()\n",
"print(f\"# Checks: {len(checks)}\")\n",
"\n",
"# List all AWS S3 checks\n",
"aws_s3_checks = CheckMetadata.list(provider=\"aws\", service=\"s3\")\n",
"print(f\"AWS S3 Checks:\\n\\t- {'\\n\\t- '.join(aws_s3_checks)}\")\n",
"\n",
"# List all critical severity checks\n",
"critical_checks = CheckMetadata.list(provider=\"aws\", severity=\"critical\")\n",
"print(f\"\\n# Critical Checks: {len(critical_checks)}\")\n",
"\n",
"# List all checks in the internet-exposed category\n",
"internet_exposed_checks = CheckMetadata.list(provider=\"aws\", category=\"internet-exposed\")\n",
"print(f\"\\n# Internet-Exposed Category Checks: {len(internet_exposed_checks)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Execute Security Scans\n",
"Set up and execute security scans on AWS resources with different filtering options."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import necessary libraries for scanning\n",
"from prowler.lib.scan.scan import Scan\n",
"# Auxiliary libraries\n",
"import itertools\n",
"\n",
"# Set up the Scan class to scan all checks for the provider\n",
"scan = Scan(provider=aws)\n",
"\n",
"# Parametrize the Scan to execute several checks, services, categories, compliances, etc.\n",
"scan_s3 = Scan(provider=aws, services=[\"s3\"], severities=[\"critical\", \"high\"])\n",
"# scan_critical = Scan(provider=aws, severities=[\"critical\"])\n",
"# scan_internet_exposed = Scan(provider=aws, categories=[\"internet-exposed\"])\n",
"\n",
"# Start the scan with the `scan` method. This returns a generator with findings and progress.\n",
"print(\"\\n##### Scanning AWS #####\")\n",
"all_findings = []\n",
"total_findings = 0\n",
"for progress, findings in scan_s3.scan():\n",
" all_findings.extend(findings)\n",
" total_findings += len(findings)\n",
" print(f\"- Scan Progress: {progress}\\n- # Findings: {total_findings}\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Process and Display Findings\n",
"Process the scan results and display detailed information about each finding."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"Finding's Details:\")\n",
"for finding in all_findings:\n",
" print(f\"\"\"\n",
" - Check ID: {finding.metadata.CheckID}\n",
" - Status: {str(finding.status)}\n",
" - Status Extended: {finding.status_extended}\n",
" - Resource ID: {finding.resource_uid}\n",
" - Resource Metadata: {finding.resource_metadata}\n",
" \"\"\")\n",
"\n",
"# Retrieve all findings in one line\n",
"print(\"\\n##### Getting all findings in one line #####\")\n",
"scan_s3 = Scan(provider=aws, services=[\"s3\"], severities=[\"critical\"])\n",
"all_findings_one_line = list(itertools.chain.from_iterable(findings for _, findings in scan_s3.scan()))\n",
"print(f\"Total findings collected in one line: {len(all_findings_one_line)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Check Metatada\n",
"```plain\n",
"CheckMetadata(\n",
" Provider='aws'\n",
" CheckID='s3_bucket_policy_public_write_access'\n",
" CheckTitle='Check if S3 buckets have policies which allow WRITE access.'\n",
" CheckType=['IAM']\n",
" CheckAliases=[]\n",
" ServiceName='s3'\n",
" SubServiceName=''\n",
" ResourceIdTemplate='arn:partition:s3:::bucket_name'\n",
" Severity=<Severity.critical: 'critical'>\n",
" ResourceType='AwsS3Bucket'\n",
" Description='Check if S3 buckets have policies which allow WRITE access.'\n",
" Risk='Non intended users can put objects in a given bucket.'\n",
" RelatedUrl=''\n",
" Remediation=\n",
" Remediation(\n",
" Code=Code(\n",
" NativeIaC=''\n",
" Terraform=''\n",
" CLI=''\n",
" Other='https://docs.prowler.com/checks/aws/s3-policies/s3_18-write-permissions-public#aws-console')\n",
" Recommendation=Recommendation(Text='Ensure proper bucket policy is in place with the least privilege principle applied.'\n",
" Url='https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3_rw-bucket.html'\n",
" )\n",
" )\n",
" Categories=['internet-exposed']\n",
" DependsOn=[]\n",
" RelatedTo=[]\n",
" Notes=''\n",
" # Compliance framework: A list of requirement IDs where the check is present.\n",
" Compliance={\n",
" \"CIS-1.10\": [\"5.2.13\"],\n",
" \"CIS-1.8\": [\"5.2.13\"]\n",
" }\n",
")\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Output Formats"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import necessary libraries for output\n",
"from prowler.lib.outputs.csv.csv import CSV\n",
"from prowler.lib.outputs.ocsf.ocsf import OCSF\n",
"from prowler.lib.outputs.asff.asff import ASFF # Only for AWS\n",
"from prowler.lib.outputs.html.html import HTML\n",
"from prowler.lib.outputs.outputs import extract_findings_statistics\n",
"import datetime\n",
"\n",
"# Get current date and time in YYYY-MM-DD_HH-MM-SS format for filenames\n",
"current_datetime = datetime.datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\")\n",
"\n",
"# Write findings to CSV file\n",
"print(\"Writing findings to CSV file...\")\n",
"csv_filename = f\"./output/findings_{current_datetime}.csv\"\n",
"csv = CSV(findings=all_findings, create_file_descriptor=True, file_path=csv_filename)\n",
"csv.batch_write_data_to_file()\n",
"print(f\"Done! CSV File Path: {csv._file_descriptor.name}\")\n",
"\n",
"# Write findings to OCSF file\n",
"print(\"Writing findings to OCSF file...\")\n",
"ocsf_filename = f\"./output/findings_{current_datetime}.ocsf\"\n",
"ocsf = OCSF(findings=all_findings, create_file_descriptor=True, file_path=ocsf_filename)\n",
"ocsf.batch_write_data_to_file()\n",
"print(f\"Done! OCSF File Path: {ocsf._file_descriptor.name}\")\n",
"\n",
"# Write findings to ASFF file\n",
"print(\"Writing findings to ASFF file...\")\n",
"asff_filename = f\"./output/findings_{current_datetime}.asff\"\n",
"asff = ASFF(findings=all_findings, create_file_descriptor=True, file_path=asff_filename)\n",
"asff.batch_write_data_to_file()\n",
"print(f\"Done! ASFF File Path: {asff._file_descriptor.name}\")\n",
"\n",
"# Write findings to HTML file\n",
"print(\"Writing findings to HTML file...\")\n",
"html_filename = f\"./output/findings_{current_datetime}.html\"\n",
"stats = extract_findings_statistics(all_findings)\n",
"html = HTML(findings=all_findings, create_file_descriptor=True, file_path=html_filename)\n",
"html.batch_write_data_to_file(provider=aws, stats=stats)\n",
"print(f\"Done! HTML File Path: {html._file_descriptor.name}\")\n",
"\n",
"# IMPORTANT: The create_file_descriptor parameter will be removed in 5.4.0\n",
"# The file descriptor will be created by default when the Output class is instantiated"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Integrate with AWS S3\n",
"Send findings to AWS S3."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the S3 class to send findings to AWS S3\n",
"from prowler.providers.aws.lib.s3.s3 import S3\n",
"\n",
"print(\"\\n##### Sending findings to S3 bucket #####\")\n",
"generated_outputs = {\"regular\": [csv, ocsf, asff, html], \"compliance\": []}\n",
"s3_integration = S3(aws.session.current_session, bucket_name=\"sdk-core\", output_directory=\"output\")\n",
"s3_integration.send_to_bucket(generated_outputs)\n",
"\n",
"# This upload the output files to the S3 bucket. In this case:\n",
"# sdk-core/output/csv/findings_2025-02-26_16-25-30.csv\n",
"# sdk-core/output/ocsf/findings_2025-02-26_16-25-30.ocsf\n",
"# sdk-core/output/asff/findings_2025-02-26_16-25-30.asff"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Integrate with AWS Security Hub\n",
"Send findings to AWS Security Hub for centralized security monitoring."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Import the SecurityHub class to send findings to AWS Security Hub\n",
"from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub\n",
"\n",
"# Print message indicating the start of the process\n",
"print(\"\\n##### Sending findings to AWS Security Hub #####\")\n",
"\n",
"# Get available AWS regions for Security Hub.\n",
"# Each finding can only be sent to Security Hub within its own region.\n",
"# Additionally, it verifies that Prowlers integration is active in\n",
"# Security Hub before sending\n",
"available_regions = aws.get_available_aws_service_regions(\n",
" \"securityhub\",\n",
" aws.identity.partition,\n",
" aws.identity.audited_regions,\n",
")\n",
"\n",
"# Initialize the SecurityHub class with necessary parameters\n",
"security_hub = SecurityHub(\n",
" aws_account_id=aws.identity.account,\n",
" aws_partition=aws.identity.partition,\n",
" aws_session=aws.session.current_session,\n",
" findings=asff.data,\n",
" send_only_fails=False,\n",
" aws_security_hub_available_regions=available_regions,\n",
")\n",
"\n",
"# Send findings to AWS Security Hub\n",
"findings_sent_to_security_hub = security_hub.batch_send_to_security_hub()\n",
"\n",
"# Print the number of findings sent to AWS Security Hub\n",
"print(f\"{findings_sent_to_security_hub} findings sent to AWS Security Hub!\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "prowler-HDV3a8VZ-py3.12",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.8"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
-13
View File
@@ -1,16 +1,3 @@
apiVersion: v1
kind: Namespace
metadata:
name: prowler-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prowler-sa
namespace: prowler-ns
---
apiVersion: batch/v1
kind: Job
metadata:
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: Namespace
metadata:
name: prowler-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: prowler-sa
namespace: prowler-ns
+4 -1
View File
@@ -94,6 +94,9 @@ nav:
- In-Cluster Execution: tutorials/kubernetes/in-cluster.md
- Non In-Cluster Execution: tutorials/kubernetes/outside-cluster.md
- Miscellaneous: tutorials/kubernetes/misc.md
- Microsoft 365:
- Authentication: tutorials/microsoft365/authentication.md
- Create Prowler Service Principal: tutorials/microsoft365/create-prowler-service-principal.md
- Developer Guide:
- Introduction: developer-guide/introduction.md
- Provider: developer-guide/provider.md
@@ -124,7 +127,7 @@ extra:
make our documentation better.
analytics:
provider: google
property: G-H5TFH6WJRQ
property: G-KBKV70W5Y2
social:
- icon: fontawesome/brands/github
link: https://github.com/prowler-cloud
Generated
+532 -405
View File
File diff suppressed because it is too large Load Diff
+48 -33
View File
@@ -51,6 +51,7 @@ from prowler.lib.outputs.compliance.cis.cis_aws import AWSCIS
from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS
from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS
from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS
from prowler.lib.outputs.compliance.cis.cis_microsoft365 import Microsoft365CIS
from prowler.lib.outputs.compliance.compliance import display_compliance_table
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
@@ -78,6 +79,7 @@ from prowler.providers.common.provider import Provider
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
from prowler.providers.gcp.models import GCPOutputOptions
from prowler.providers.kubernetes.models import KubernetesOutputOptions
from prowler.providers.microsoft365.models import Microsoft365OutputOptions
def prowler():
@@ -259,6 +261,10 @@ def prowler():
output_options = KubernetesOutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
elif provider == "microsoft365":
output_options = Microsoft365OutputOptions(
args, bulk_checks_metadata, global_provider.identity
)
# Run the quick inventory for the provider if available
if hasattr(args, "quick_inventory") and args.quick_inventory:
@@ -299,15 +305,26 @@ def prowler():
print(f"{Style.BRIGHT}{Fore.GREEN}\nNo findings to fix!{Style.RESET_ALL}\n")
sys.exit()
# Outputs
# TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding
# This will be refactored for the outputs generate directly the Finding
finding_outputs = []
for finding in findings:
try:
finding_outputs.append(
Finding.generate_output(global_provider, finding, output_options)
)
except Exception:
continue
# Extract findings stats
stats = extract_findings_statistics(findings)
stats = extract_findings_statistics(finding_outputs)
if args.slack:
# TODO: this should be also in a config file
if "SLACK_API_TOKEN" in environ and (
"SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ
):
token = environ["SLACK_API_TOKEN"]
channel = (
environ["SLACK_CHANNEL_NAME"]
@@ -324,26 +341,16 @@ def prowler():
)
sys.exit(1)
# Outputs
# TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding
# This will be refactored for the outputs generate directly the Finding
finding_outputs = [
Finding.generate_output(global_provider, finding, output_options)
for finding in findings
]
generated_outputs = {"regular": [], "compliance": []}
if args.output_formats:
for mode in args.output_formats:
filename = (
f"{output_options.output_directory}/"
f"{output_options.output_filename}"
f"{output_options.output_directory}/{output_options.output_filename}"
)
if mode == "csv":
csv_output = CSV(
findings=finding_outputs,
create_file_descriptor=True,
file_path=f"{filename}{csv_file_suffix}",
)
generated_outputs["regular"].append(csv_output)
@@ -353,7 +360,6 @@ def prowler():
if mode == "json-asff":
asff_output = ASFF(
findings=finding_outputs,
create_file_descriptor=True,
file_path=f"{filename}{json_asff_file_suffix}",
)
generated_outputs["regular"].append(asff_output)
@@ -363,7 +369,6 @@ def prowler():
if mode == "json-ocsf":
json_output = OCSF(
findings=finding_outputs,
create_file_descriptor=True,
file_path=f"{filename}{json_ocsf_file_suffix}",
)
generated_outputs["regular"].append(json_output)
@@ -371,7 +376,6 @@ def prowler():
if mode == "html":
html_output = HTML(
findings=finding_outputs,
create_file_descriptor=True,
file_path=f"{filename}{html_file_suffix}",
)
generated_outputs["regular"].append(html_output)
@@ -394,7 +398,6 @@ def prowler():
cis = AWSCIS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(cis)
@@ -408,7 +411,6 @@ def prowler():
mitre_attack = AWSMitreAttack(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(mitre_attack)
@@ -422,7 +424,6 @@ def prowler():
ens = AWSENS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(ens)
@@ -436,7 +437,6 @@ def prowler():
aws_well_architected = AWSWellArchitected(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(aws_well_architected)
@@ -450,7 +450,6 @@ def prowler():
iso27001 = AWSISO27001(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(iso27001)
@@ -464,7 +463,6 @@ def prowler():
kisa_ismsp = AWSKISAISMSP(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(kisa_ismsp)
@@ -477,7 +475,6 @@ def prowler():
generic_compliance = GenericCompliance(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(generic_compliance)
@@ -494,7 +491,6 @@ def prowler():
cis = AzureCIS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(cis)
@@ -508,7 +504,6 @@ def prowler():
mitre_attack = AzureMitreAttack(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(mitre_attack)
@@ -522,7 +517,6 @@ def prowler():
ens = AzureENS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(ens)
@@ -535,7 +529,6 @@ def prowler():
generic_compliance = GenericCompliance(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(generic_compliance)
@@ -552,7 +545,6 @@ def prowler():
cis = GCPCIS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(cis)
@@ -566,7 +558,6 @@ def prowler():
mitre_attack = GCPMitreAttack(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(mitre_attack)
@@ -580,7 +571,6 @@ def prowler():
ens = GCPENS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(ens)
@@ -593,7 +583,6 @@ def prowler():
generic_compliance = GenericCompliance(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(generic_compliance)
@@ -610,7 +599,6 @@ def prowler():
cis = KubernetesCIS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(cis)
@@ -623,7 +611,34 @@ def prowler():
generic_compliance = GenericCompliance(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
create_file_descriptor=True,
file_path=filename,
)
generated_outputs["compliance"].append(generic_compliance)
generic_compliance.batch_write_data_to_file()
elif provider == "microsoft365":
for compliance_name in input_compliance_frameworks:
if compliance_name.startswith("cis_"):
# Generate CIS Finding Object
filename = (
f"{output_options.output_directory}/compliance/"
f"{output_options.output_filename}_{compliance_name}.csv"
)
cis = Microsoft365CIS(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
file_path=filename,
)
generated_outputs["compliance"].append(cis)
cis.batch_write_data_to_file()
else:
filename = (
f"{output_options.output_directory}/compliance/"
f"{output_options.output_filename}_{compliance_name}.csv"
)
generic_compliance = GenericCompliance(
findings=finding_outputs,
compliance=bulk_compliance_frameworks[compliance_name],
file_path=filename,
)
generated_outputs["compliance"].append(generic_compliance)
@@ -28,7 +28,9 @@
"Service": "ebs"
}
],
"Checks": []
"Checks": [
"ec2_ebs_volume_snapshots_exists"
]
},
{
"Id": "1.0.3",
@@ -42,7 +44,8 @@
}
],
"Checks": [
"ec2_ebs_default_encryption"
"ec2_ebs_default_encryption",
"ec2_ebs_volume_encryption"
]
},
{
@@ -87,7 +90,9 @@
}
],
"Checks": [
"iam_user_mfa_enabled_console_access"
"iam_user_mfa_enabled_console_access",
"iam_user_hardware_mfa_enabled",
"iam_root_mfa_enabled"
]
},
{
@@ -102,7 +107,9 @@
}
],
"Checks": [
"iam_user_mfa_enabled_console_access"
"iam_user_mfa_enabled_console_access",
"iam_user_hardware_mfa_enabled",
"iam_root_mfa_enabled"
]
},
{
@@ -117,7 +124,9 @@
}
],
"Checks": [
"iam_root_mfa_enabled"
"iam_root_mfa_enabled",
"iam_root_hardware_mfa_enabled",
"iam_user_mfa_enabled_console_access"
]
},
{
@@ -162,7 +171,10 @@
}
],
"Checks": [
"rds_instance_no_public_access"
"rds_instance_no_public_access",
"s3_bucket_public_access",
"s3_bucket_public_list_acl",
"s3_account_level_public_access_blocks"
]
},
{
@@ -192,7 +204,8 @@
}
],
"Checks": [
"rds_instance_storage_encrypted"
"rds_instance_storage_encrypted",
"rds_instance_transport_encrypted"
]
},
{
+2 -1
View File
@@ -584,7 +584,8 @@
"Id": "2.3.1",
"Description": "Ensure that encryption is enabled for RDS Instances",
"Checks": [
"rds_instance_storage_encrypted"
"rds_instance_storage_encrypted",
"rds_instance_transport_encrypted"
],
"Attributes": [
{
+2 -1
View File
@@ -584,7 +584,8 @@
"Id": "2.3.1",
"Description": "Ensure that encryption is enabled for RDS Instances",
"Checks": [
"rds_instance_storage_encrypted"
"rds_instance_storage_encrypted",
"rds_instance_transport_encrypted"
],
"Attributes": [
{
+9 -4
View File
@@ -303,7 +303,9 @@
{
"Id": "1.22",
"Description": "Ensure access to AWSCloudShellFullAccess is restricted",
"Checks": [],
"Checks": [
"iam_policy_cloudshell_admin_not_attached"
],
"Attributes": [
{
"Section": "1. Identity and Access Management",
@@ -492,7 +494,8 @@
"Id": "2.1.2",
"Description": "Ensure MFA Delete is enabled on S3 buckets",
"Checks": [
"s3_bucket_no_mfa_delete"
"s3_bucket_no_mfa_delete",
"cloudtrail_bucket_requires_mfa_delete"
],
"Attributes": [
{
@@ -581,7 +584,8 @@
"Id": "2.3.1",
"Description": "Ensure that encryption is enabled for RDS Instances",
"Checks": [
"rds_instance_storage_encrypted"
"rds_instance_storage_encrypted",
"rds_instance_transport_encrypted"
],
"Attributes": [
{
@@ -1347,7 +1351,8 @@
"Id": "5.6",
"Description": "Ensure that EC2 Metadata Service only allows IMDSv2",
"Checks": [
"ec2_instance_imdsv2_enabled"
"ec2_instance_imdsv2_enabled",
"ec2_instance_account_imdsv2_enabled"
],
"Attributes": [
{
File diff suppressed because it is too large Load Diff

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