Compare commits

...

353 Commits

Author SHA1 Message Date
Adrián Jesús Peña Rodríguez
4a14e21553 Merge branch 'master' into PRWLR-5956-Export-Artifacts 2025-02-04 10:31:50 +01:00
Pedro Martín
5f54377407 chore(aws_audit_manager_control_tower_guardrails): add checks to reqs (#6699) 2025-02-03 14:59:08 -05:00
Drew Kerrigan
552aa64741 docs(): add description of changed and new delta values to prowler app tutorial (#6801) 2025-02-03 20:51:03 +01:00
dependabot[bot]
d64f611f51 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-03 12:48:18 -05:00
dependabot[bot]
a96cc92d77 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-03 11:37:02 -05:00
dependabot[bot]
3858cccc41 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-03 10:32:42 -05:00
Pedro Martín
072828512a fix(cis_1.5_aws): add checks to needed reqs (#6695)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-03 10:32:20 -05:00
Pedro Martín
a73ffe5642 fix(cis_1.4_aws): add checks to needed reqs (#6696)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-02-03 10:32:10 -05:00
Pablo Lara
8e784a5b6d feat(scans): show scan details right after launch (#6791) 2025-02-03 16:08:47 +01:00
dependabot[bot]
1b6f9332f1 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-03 09:35:53 -05:00
secretcod3r
db8b472729 fix(gcp): fix wrong provider value in check (#6691) 2025-02-03 09:29:08 -05:00
Pedro Martín
867b371522 fix(cis_2.0_aws): add checks to needed reqs (#6694) 2025-02-03 09:28:04 -05:00
dependabot[bot]
c0d7c9fc7d 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-03 09:27:17 -05:00
Pablo Lara
bb4685cf90 fix(findings): remove default status filtering (#6784) 2025-02-03 15:20:18 +01:00
Pablo Lara
6a95426749 fix(findings): order findings by inserted_at DESC (#6782) 2025-02-03 11:51:07 +01:00
Víctor Fernández Poyatos
ef6af8e84d feat(schedules): Rework daily schedule to always show the next scan (#6700) 2025-02-03 11:08:27 +01:00
Víctor Fernández Poyatos
763130f253 fix(celery): Kill celery worker process after every task to release memory (#6761) 2025-01-31 19:30:08 +05:45
Hugo Pereira Brito
1256c040e9 fix: microsoft365 mutelist (#6724) 2025-01-31 12:32:39 +01:00
Adrián Jesús Peña Rodríguez
0995c7a845 fix: scan unittests 2025-01-31 11:30:09 +01:00
dependabot[bot]
18b7b48a99 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-01-31 10:07:17 +01:00
Pepe Fagoaga
627c11503f fix(db_event): Handle other events (#6754) 2025-01-30 21:46:43 +05:45
Víctor Fernández Poyatos
712ba84f06 feat(scans): Optimize read queries during scans (#6753) 2025-01-30 20:51:12 +05:45
Pepe Fagoaga
5186e029b3 fix(set_report_color): Add more details to error (#6751) 2025-01-30 20:48:51 +05:45
Pablo Lara
5bfaedf903 fix: Enable hot reloading when using Docker Compose for UI (#6750) 2025-01-30 14:05:39 +01:00
Víctor Fernández Poyatos
5061da6897 feat(findings): Improve /findings/metadata performance (#6748) 2025-01-30 13:31:43 +01:00
Adrián Jesús Peña Rodríguez
63f8186bd6 fix: resolve ruff errors 2025-01-30 12:46:53 +01:00
Pepe Fagoaga
c159a28016 fix(neptune): correct service name (#6743) 2025-01-30 17:16:18 +05:45
Adrián Jesús Peña Rodríguez
d70c71c903 chore: findings fix restore 2025-01-30 12:18:51 +01:00
Adrián Jesús Peña Rodríguez
53c571c289 Merge branch 'master' into PRWLR-5956-Export-Artifacts 2025-01-30 11:59:54 +01:00
Adrián Jesús Peña Rodríguez
a3c7846cd9 feat(report): add export system and its endpoint 2025-01-30 11:54:44 +01:00
Pepe Fagoaga
82a1b1c921 fix(finding): raise when generating invalid findings (#6738) 2025-01-30 15:59:38 +05:45
Pepe Fagoaga
bf2210d0f4 fix(acm): Key Error DomainName (#6739) 2025-01-30 15:54:31 +05:45
Kay Agahd
8f0772cb94 fix(aws): iam_user_with_temporary_credentials resource in OCSF (#6697)
Co-authored-by: Pepe Fagoaga <pepe@verica.io>
2025-01-30 15:28:21 +05:45
Pepe Fagoaga
5b57079ecd fix(sns): Add region to subscriptions (#6731) 2025-01-30 14:38:21 +05:45
Matt Johnson
350d759517 chore: Update Google Analytics ID across all docs.prowler.com sites. (#6730) 2025-01-30 12:47:01 +05:45
Pablo Lara
edd793c9f5 fix(scans): change label for next scan (#6725) 2025-01-29 10:46:49 +01:00
Víctor Fernández Poyatos
545c2dc685 fix(migrations): Use indexes instead of constraints to define an index (#6722) 2025-01-29 14:24:04 +05:45
Víctor Fernández Poyatos
84955c066c revert: Update Django DB manager to use psycopg3 and connection pooling (#6717) 2025-01-28 22:15:01 +05:45
Víctor Fernández Poyatos
06dd03b170 fix(scan-summaries): Improve efficiency on providers overview (#6716) 2025-01-28 21:56:29 +05:45
Pedro Martín
47bc2ed2dc fix(defender): add field to SecurityContacts (#6693) 2025-01-28 15:52:56 +01:00
Pablo Lara
44281afc54 fix(scans): filters and sorting for scan table (#6713) 2025-01-28 13:26:31 +01:00
Víctor Fernández Poyatos
4d2859d145 fix(scans, findings): Improve API performance ordering by inserted_at instead of id (#6711) 2025-01-28 16:41:58 +05:45
Pablo Lara
45d44a1669 fix: fixed bug when opening finding details while a scan is in progress (#6708) 2025-01-28 06:58:18 +01:00
dependabot[bot]
ddd83b340e 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-01-26 13:39:42 +01:00
Mario Rodriguez Lopez
ccdb54d7c3 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-01-24 13:14:17 -05:00
Rubén De la Torre Vico
bcc246d950 fix(cloudsql): add trusted client certificates case for cloudsql_instance_ssl_connections (#6682) 2025-01-24 10:42:45 -05:00
dependabot[bot]
62139e252a 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-01-24 12:40:11 +01:00
dependabot[bot]
86950c3a0a 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-01-24 10:47:09 +01:00
dependabot[bot]
f4865ef68d 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-01-24 09:44:16 +01:00
Pepe Fagoaga
ea7209e7ae chore: bump for next minor (#6672) 2025-01-23 13:13:08 -05:00
Hugo Pereira Brito
998c551cf3 fix(cloudwatch): NoneType object is not iterable (#6671) 2025-01-23 12:27:07 -05:00
Paolo Frigo
e6f29b0116 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-01-23 11:11:03 -05:00
Pepe Fagoaga
eb90bb39dc chore(api): Bump to v1.3.0 (#6670) 2025-01-23 21:25:29 +05:45
Pepe Fagoaga
ad189b35ad chore(scan): Remove ._findings (#6667) 2025-01-23 20:43:02 +05:45
Pablo Lara
7d2989a233 chore: adjust DateWithTime component height when used with InfoField (#6669) 2025-01-23 15:18:24 +01:00
Pablo Lara
862137ae7d chore(scans): improve scan details (#6665) 2025-01-23 13:20:41 +01:00
Pedro Martín
c86e082d9a feat(detect-secrets): get secrets plugins from config.yaml (#6544)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-23 17:18:19 +05:45
Sergio Garcia
80fe048f97 feat(resource metadata): add resource metadata to JSON OCSF (#6592)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
2025-01-23 16:06:30 +05:45
dependabot[bot]
f2bffb3ce7 chore(deps): bump azure-mgmt-containerservice from 33.0.0 to 34.0.0 (#6630)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 16:37:07 -05:00
dependabot[bot]
cbe2f9eef8 chore(deps): bump azure-mgmt-compute from 33.1.0 to 34.0.0 (#6628)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 20:00:56 +01:00
Pepe Fagoaga
688f41f570 fix(templates): Customize principals and add validation (#6655) 2025-01-22 21:47:57 +05:45
Anton Rubets
a29197637e chore(helm): Add prowler helm support (#6580)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-22 10:55:26 -05:00
Prowler Bot
7a2712a37f chore(regions_update): Changes in regions for AWS services (#6652)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-22 09:30:03 -05:00
dependabot[bot]
189f5cfd8c chore(deps): bump boto3 from 1.35.94 to 1.35.99 (#6651)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 09:29:41 -05:00
Kay Agahd
e509480892 fix: add detector and line number of potential secret (#6654) 2025-01-22 20:13:23 +05:45
Pepe Fagoaga
7f7955351a chore(pre-commit): poetry checks for API and SDK (#6658) 2025-01-22 20:05:26 +05:45
Pepe Fagoaga
46f1db21a8 chore(api): Use prowler from master (#6657) 2025-01-22 20:05:02 +05:45
Pablo Lara
fbe7bc6951 feat(providers): show the cloud formation and terraform template links on the form (#6660) 2025-01-22 14:49:38 +01:00
Pablo Lara
f658507847 feat(providers): make external id field mandatory in the aws role secret form (#6656) 2025-01-22 12:45:31 +01:00
dependabot[bot]
374078683b chore(deps-dev): bump moto from 5.0.16 to 5.0.27 (#6632)
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-01-21 13:56:06 -05:00
dependabot[bot]
114c4e0886 chore(deps): bump botocore from 1.35.94 to 1.35.99 (#6520)
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-01-21 09:17:18 -05:00
Pablo Lara
67c62766d4 fix(filters): fix dynamic filters (#6642) 2025-01-21 13:33:27 +01:00
dependabot[bot]
3f2947158d chore(deps): bump prowler from 5.1.1 to 5.1.4 in /api (#6641)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 14:27:59 +05:45
dependabot[bot]
278a7cb356 chore(deps-dev): bump mkdocs-material from 9.5.49 to 9.5.50 (#6631)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 18:31:44 -05:00
Rubén De la Torre Vico
890158a79c fix(OCSF): fix OCSF output when timestamp is UNIX format (#6606) 2025-01-20 17:11:28 -05:00
Rubén De la Torre Vico
4dc1602b77 fix: update Azure CIS with existing App checks (#6611) 2025-01-20 15:12:00 -05:00
Kay Agahd
bbba0abac9 fix(aws): list tags for DocumentDB clusters (#6605) 2025-01-20 15:10:58 -05:00
Prowler Bot
d04fd807c6 chore(regions_update): Changes in regions for AWS services (#6599)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-20 15:09:35 -05:00
Pablo Lara
3456df4cf1 fix(snippet-id): improve provider ID readability in tables (#6615) 2025-01-20 17:23:19 +01:00
Pablo Lara
f56aaa791e chore(RBAC): add permission's info (#6612) 2025-01-20 16:14:48 +01:00
Adrián Jesús Peña Rodríguez
465a758770 fix(rbac): remove invalid required permission (#6608) 2025-01-20 15:21:52 +01:00
Pablo Lara
0f7c0c1b2c fix(RBAC): tweaks for edit role form (#6609) 2025-01-20 14:09:16 +01:00
Adrián Jesús Peña Rodríguez
bf8d10b6f6 feat(api): restrict the deletion of users, only the user of the request can be deleted (#6607) 2025-01-20 13:26:47 +01:00
Pablo Lara
20d04553d6 fix(RBAC): restore manage_account permission for roles (#6602) 2025-01-20 11:35:29 +01:00
Daniel Barranquero
b56d62e3c4 fix(sqs): fix flaky test (#6593) 2025-01-17 11:48:39 -05:00
Hugo Pereira Brito
9a332dcba1 chore(services): delete all comment headers (#6585) 2025-01-17 08:21:28 -05:00
Hugo Pereira Brito
166d9f8823 fix(apigatewayv2): managed exception NotFoundException (#6576) 2025-01-17 08:17:51 -05:00
Prowler Bot
42f5eed75f chore(regions_update): Changes in regions for AWS services (#6577)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-17 08:17:00 -05:00
Rubén De la Torre Vico
01a7db18dd fix: add missing Check_Report_Azure parameters (#6583) 2025-01-17 08:16:43 -05:00
Pablo Lara
d4507465a3 fix(providers): update the label and placeholder based on the cloud provider (#6581) 2025-01-17 12:28:38 +01:00
Pablo Lara
3ac92ed10a fix(findings): remove filter delta_in applied by default (#6578) 2025-01-17 11:03:12 +01:00
Pablo Lara
43c76ca85c feat(findings): add first seen in findings details (#6575) 2025-01-17 10:19:10 +01:00
dependabot[bot]
54d87fa96a chore(deps): bump prowler from 5.0.2 to 5.1.1 in /api (#6573)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-17 13:26:07 +05:45
Daniel Barranquero
f041f17268 fix(gcp): fix flaky tests from dns service (#6569) 2025-01-16 14:49:25 -05:00
dependabot[bot]
31c80a6967 chore(deps): bump msgraph-sdk from 1.16.0 to 1.17.0 (#6547)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 12:55:30 -05:00
Rubén De la Torre Vico
783ce136f4 feat(network): extract Network resource metadata automated (#6555)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-16 12:41:02 -05:00
Rubén De la Torre Vico
f829145781 feat(storage): extract Storage resource metadata automated (#6563)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-16 11:44:43 -05:00
Rubén De la Torre Vico
389337f8cd feat(vm): extract VM resource metadata automated (#6564) 2025-01-16 11:16:02 -05:00
Pedro Martín
a0713c2d66 fix(cis): add subsections if needed (#6559) 2025-01-16 11:10:54 -05:00
Rubén De la Torre Vico
f94d3cbce4 feat(sqlserver): extract SQL Server resource metadata automated (#6562) 2025-01-16 10:47:21 -05:00
Daniel Barranquero
8d8994b468 feat(aws): include resource metadata to remaining checks (#6551)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-16 10:44:14 -05:00
Rubén De la Torre Vico
784a9097a5 feat(postgresql): extract PostgreSQL resource metadata automated (#6560) 2025-01-16 10:37:55 -05:00
Pedro Martín
b9601626e3 fix(detect_secrets): refactor logic for detect-secrets (#6537) 2025-01-16 21:15:44 +05:45
Rubén De la Torre Vico
dc80b011f2 feat(policy): extract Policy resource metadata automated (#6558) 2025-01-16 10:29:28 -05:00
Rubén De la Torre Vico
ee7d32d460 feat(entra): extract Entra resource metadata automated (#6542)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-16 10:24:53 -05:00
Rubén De la Torre Vico
43fd9ee94e feat(monitor): extract monitor resource metadata automated (#6554)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-16 10:16:19 -05:00
Víctor Fernández Poyatos
8821a91f3f feat(db): Update Django DB manager to use psycopg3 and connection pooling (#6541) 2025-01-16 15:29:02 +01:00
Rubén De la Torre Vico
98d9256f92 feat(mysql): extract MySQL resource metadata automated (#6556) 2025-01-16 09:24:06 -05:00
Rubén De la Torre Vico
b35495eaa7 feat(keyvault): extract KeyVault resource metadata automated (#6553) 2025-01-16 09:17:36 -05:00
Rubén De la Torre Vico
74d6b614b3 feat(iam): extract IAM resource metadata automated (#6552) 2025-01-16 09:05:23 -05:00
Sergio Garcia
dd63c16a74 fix(gcp): iterate through service projects (#6549)
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2025-01-16 08:52:52 -05:00
Pablo Lara
4280266a96 fix(dep): address compatibility issues (#6543) 2025-01-16 14:28:49 +01:00
Hugo Pereira Brito
b1f02098ff feat(aws): include resource metadata in services from r* to s* (#6536)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-15 18:10:53 -05:00
Pedro Martín
95189b574a feat(gcp): add resource metadata to report (#6500)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-15 18:09:35 -05:00
Hugo Pereira Brito
c5d23503bf feat(aws): include resource metadata in services from a* to b* (#6504)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-15 18:03:37 -05:00
Daniel Barranquero
77950f6069 chore(aws): add resource metadata to services from t to w (#6546) 2025-01-15 17:22:08 -05:00
Daniel Barranquero
ec5f2b3753 chore(aws): add resource metadata to services from f to o (#6545) 2025-01-15 17:15:50 -05:00
Rubén De la Torre Vico
9e7104fb7f feat(defender): extract Defender resource metadata in automated way (#6538) 2025-01-15 12:14:24 -05:00
Rubén De la Torre Vico
6b3b6ca45e feat(appinsights): extract App Insights resource metadata in automated way (#6540) 2025-01-15 11:45:23 -05:00
Hugo Pereira Brito
20b8b0b24e feat: add resource metadata to emr_cluster_account_public_block_enabled (#6539) 2025-01-15 11:44:51 -05:00
Sergio Garcia
4e11540458 feat(kubernetes): add resource metadata to report (#6479) 2025-01-15 11:36:09 -05:00
Hugo Pereira Brito
ee87f2676d feat(aws): include resource metadata in services from d* to e* (#6532)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-15 10:05:04 -05:00
Daniel Barranquero
74a90aab98 feat(aws): add resource metadata to all services starting with c (#6493) 2025-01-15 09:04:19 -05:00
Rubén De la Torre Vico
48ff9a5100 feat(cosmosdb): extract CosmosDB resource metadata in automated way (#6533) 2025-01-15 08:51:48 -05:00
Rubén De la Torre Vico
3dfd578ee5 feat(containerregistry): extract Container Registry resource metadata in automated way (#6530) 2025-01-15 08:51:16 -05:00
Rubén De la Torre Vico
0db46cdc81 feat(azure-app): extract Web App resource metadata in automated way (#6529) 2025-01-15 08:48:36 -05:00
Prowler Bot
fdac58d031 chore(regions_update): Changes in regions for AWS services (#6526)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-15 08:46:35 -05:00
dependabot[bot]
df9d4ce856 chore(deps): bump google-api-python-client from 2.158.0 to 2.159.0 (#6521)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-15 08:33:47 -05:00
Pedro Martín
e6ae4e97e8 docs(readme): update pr template to add check for readme (#6531) 2025-01-15 12:12:45 +01:00
Adrián Jesús Peña Rodríguez
10a4c28922 feat(finding): add first_seen attribute (#6460) 2025-01-15 11:25:41 +01:00
dependabot[bot]
8a828c6e51 chore(deps): bump django from 5.1.4 to 5.1.5 in /api (#6519)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-15 10:52:11 +01:00
Víctor Fernández Poyatos
d7b40905ff feat(findings): Add resource_tag filters for findings endpoint (#6527) 2025-01-15 10:30:36 +01:00
Adrián Jesús Peña Rodríguez
f9a3b5f3cd feat(provider-secret): make existing external_id field mandatory (#6510) 2025-01-15 10:14:44 +01:00
Pablo Lara
b73b89242f feat(filters): add resource type filter for findings (#6524) 2025-01-15 08:40:53 +01:00
dependabot[bot]
23a0f6e8de chore(deps-dev): bump eslint-config-prettier from 9.1.0 to 10.0.1 in /ui (#6518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-15 06:55:25 +01:00
Pedro Martín
87967abc3f feat(kubernetes): add CIS 1.10 compliance (#6508)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-14 14:16:00 -05:00
Rubén De la Torre Vico
ce60c286dc feat(aks): use Check_Report_Azure constructor properly in AKS checks (#6509) 2025-01-14 14:14:02 -05:00
Pepe Fagoaga
90fd9b0eb8 chore(version): set next minor (#6511) 2025-01-14 14:06:24 -05:00
Prowler Bot
ca262a6797 chore(regions_update): Changes in regions for AWS services (#6495)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-14 12:43:44 -05:00
Rubén De la Torre Vico
c056d39775 feat(aisearch): use Check_Report_Azure constructor properly in AISearch checks (#6506) 2025-01-14 12:37:01 -05:00
johannes-engler-mw
1c4426ea4b fix(Azure TDE): add filter for master DB (#6351) 2025-01-14 12:34:52 -05:00
Pedro Martín
36520bd7a1 feat(azure): add CIS 3.0 for Azure (#5226) 2025-01-14 12:07:22 -05:00
Pepe Fagoaga
badf0ace76 feat(prowler-role): Add templates to deploy it in AWS (#6499) 2025-01-14 12:04:20 -05:00
Rubén De la Torre Vico
f1f61249e0 feat(azure): include resource metadata in Check_Report_Azure (#6505) 2025-01-14 11:32:40 -05:00
dependabot[bot]
b371cac18c chore(deps): bump jinja2 from 3.1.4 to 3.1.5 (#6457)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 10:03:45 -05:00
Víctor Fernández Poyatos
1846535d8d feat(findings): add /findings/metadata to retrieve dynamic filters information (#6503) 2025-01-14 15:30:03 +01:00
dependabot[bot]
d7d9118b9b chore(deps-dev): bump bandit from 1.8.0 to 1.8.2 (#6485)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 08:49:37 -05:00
Pablo Lara
a65ca72177 chore(groups): Enable updating groups without roles or providers (#6498) 2025-01-14 11:16:13 +01:00
Pablo Lara
1108d90768 chore(roles): prevent capitalization of provider groups and roles (#6497) 2025-01-14 10:41:08 +01:00
Adrián Jesús Peña Rodríguez
6715aa351f fix(rbac): block admin role deletion (#6470) 2025-01-14 10:27:41 +01:00
dependabot[bot]
851497eb0a chore(deps): bump @radix-ui/react-slot from 1.1.0 to 1.1.1 in /ui (#6481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 10:25:14 +01:00
dependabot[bot]
3bb4663e3e chore(deps-dev): bump eslint-plugin-import from 2.29.1 to 2.31.0 in /ui (#6482)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 10:24:31 +01:00
Pablo Lara
6953fcf6b5 chore(rbac): tweaks role permissions (#6496) 2025-01-14 10:23:23 +01:00
Adrián Jesús Peña Rodríguez
ab844eee3f ref(rbac): disable some checks (#6471) 2025-01-14 09:33:15 +01:00
Pedro Martín
708e06aa3b fix(iso27001-2013): add ReqId and ReqDescription in output (#6405) 2025-01-13 13:14:09 -05:00
Prowler Bot
aa8b8bbcae chore(regions_update): Changes in regions for AWS services (#6459)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-13 12:41:11 -05:00
Pablo Lara
0ce1e15c2c styles(invitations): tweak styles for invitation details box (#6475) 2025-01-13 18:32:33 +01:00
Pablo Lara
105a83d946 fix(invitation): correct the URL used to share an invitation (#6472) 2025-01-13 17:27:10 +01:00
Pedro Martín
e9a885a54d feat(compliance): add CIS 3.0 for gcp (#6463) 2025-01-13 10:59:53 -05:00
Pablo Lara
0a8759ee06 chore(manage-groups): tweaks for provider manage groups (#6468) 2025-01-13 16:39:14 +01:00
Prowler Bot
33ec21bbac chore(regions_update): Changes in regions for AWS services (#6458)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-13 10:37:43 -05:00
dependabot[bot]
7c00f65ecc chore(deps): bump @radix-ui/react-toast from 1.2.1 to 1.2.4 in /ui (#6445)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 15:55:56 +01:00
Rubén De la Torre Vico
7777c8f135 fix(vpc): add new principal wildcard verification (#6461) 2025-01-13 09:49:10 -05:00
dependabot[bot]
2386490002 chore(deps-dev): bump openapi-schema-validator from 0.6.2 to 0.6.3 (#6454)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 09:36:31 -05:00
Pepe Fagoaga
b620f12027 chore(rls): Add tenant_id filters in views and improve querysets (#6211)
Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
2025-01-13 11:37:40 +01:00
Rubén De la Torre Vico
00722181ad docs(azure): improve tutorials for Prowler App (#6210) 2025-01-13 09:59:58 +01:00
Sergio Garcia
15e888a939 feat(ec2): include resource metadata in Check_Report (#6440) 2025-01-13 13:04:55 +05:45
dependabot[bot]
43fa600f1c chore(deps): bump date-fns from 3.6.0 to 4.1.0 in /ui (#6444)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 08:35:18 +01:00
dependabot[bot]
2e4b5399c9 chore(deps): bump lucide-react from 0.417.0 to 0.471.0 in /ui (#6456)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 08:34:47 +01:00
Prowler Bot
62cbb442e8 chore(regions_update): Changes in regions for AWS services (#6448)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-10 11:36:43 -05:00
Pedro Martín
b0fe696935 refactor(mutelist): use jsonschema on mutelist (#6264) 2025-01-10 20:04:20 +05:45
Matt Johnson
42dbefbb31 feat: New gen-ai category for all relevant checks. (#6450) 2025-01-10 08:57:20 -05:00
Daniel Barranquero
f3dbe28681 fix(codeartifact): fix flaky tests (#6449) 2025-01-10 18:16:00 +05:45
Pedro Martín
6a5f1a7839 docs(integrations): add integrations docs (#6269) 2025-01-10 17:00:20 +05:45
Pedro Martín
3b70f9fed4 docs(outputs): add custom outputs formats documentation (#6386) 2025-01-10 16:54:50 +05:45
dependabot[bot]
7eb01aaa5c chore(deps-dev): bump safety from 3.2.3 to 3.2.9 in /api (#6431)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 16:31:19 +05:45
dependabot[bot]
1e27e52fba chore(deps-dev): bump vulture from 2.11 to 2.14 in /api (#6426)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 15:14:55 +05:45
dependabot[bot]
16d73619e4 chore(deps): bump boto3 from 1.35.93 to 1.35.94 (#6410)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 19:29:59 -05:00
dependabot[bot]
bc82696f15 chore(deps): bump google-api-python-client from 2.157.0 to 2.158.0 (#6442)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 18:16:23 -05:00
dependabot[bot]
fdb90623fc chore(deps): bump trufflesecurity/trufflehog from 3.88.1 to 3.88.2 (#6446)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 17:52:12 -05:00
Prowler Bot
5fa62a9770 chore(regions_update): Changes in regions for AWS services (#6399)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-09 14:57:30 -05:00
Pablo Lara
8f3df7e45d fix(BC: NextUI): fix BC from NextUI, resolve ESLint warnings and optimize hooks dependencies (#6404) 2025-01-09 17:37:33 +01:00
dependabot[bot]
bb417587ae chore(deps-dev): bump @iconify/react from 5.0.1 to 5.2.0 in /ui (#6421)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 17:05:11 +01:00
dependabot[bot]
6b6e12cea3 chore(deps): bump jinja2 from 3.1.4 to 3.1.5 in /api (#6316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 20:39:02 +05:45
Pepe Fagoaga
65e70b2ca4 chore(dependabot): Review for API and UI (#6402) 2025-01-09 20:28:26 +05:45
Pepe Fagoaga
94d25f6f6a chore(containers): Build stable for API and UI (#6395) 2025-01-09 20:24:57 +05:45
Sergio Garcia
4bcf036831 fix(iam): handle non existing MFA devices (#6396) 2025-01-09 09:23:05 -05:00
dependabot[bot]
901bc69a7d chore(deps): bump django from 5.1.1 to 5.1.4 in /api (#6376)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 13:36:58 +01:00
Adrián Jesús Peña Rodríguez
465217442b fix(api): change the inserted_at.lte unittest (#6403) 2025-01-09 13:12:55 +01:00
Pablo Lara
e6b40358aa feat(update-credentials): add explanation text for the current behavior (#6400) 2025-01-09 11:13:36 +01:00
Daniel Barranquero
9d48f7286a fix(cloudformation): fix flaky tests (#6398) 2025-01-09 15:30:11 +05:45
Prowler Bot
80311d3837 chore(regions_update): Changes in regions for AWS services (#6390)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2025-01-08 12:28:24 -05:00
Pedro Martín
f501149068 fix(pre-commit): add api needed excludes (#6393) 2025-01-08 16:34:55 +01:00
dependabot[bot]
750de62828 chore(deps): bump botocore from 1.35.93 to 1.35.94 (#6388)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-08 10:32:34 -05:00
Pablo Lara
d2f338ceb6 feat(scans): add new component - alert bar 2025-01-08 11:01:52 +01:00
dependabot[bot]
e8d66979b3 chore(deps): bump azure-mgmt-network from 28.0.0 to 28.1.0 (#6296)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 16:09:53 -05:00
Sergio Garcia
b5180389f8 feat(aws): add new check cloudformation_stack_cdktoolkit_bootstrap_version (#6323) 2025-01-07 14:52:55 -05:00
dependabot[bot]
fbd5235e15 chore(deps): bump msgraph-sdk from 1.15.0 to 1.16.0 (#6350)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 13:56:02 -05:00
dependabot[bot]
afd2267c26 chore(deps): bump microsoft-kiota-abstractions from 1.6.7 to 1.6.8 (#6347)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 11:40:57 -05:00
dependabot[bot]
9e798ababd chore(deps): bump google-api-python-client from 2.156.0 to 2.157.0 (#6349)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 10:40:20 -05:00
Prowler Bot
e9f2fc8ee1 chore(regions_update): Changes in regions for AWS services (#6382)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-07 10:11:24 -05:00
dependabot[bot]
12198b4f06 chore(deps): bump boto3 from 1.35.87 to 1.35.93 (#6381)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 09:20:20 -05:00
Adrián Jesús Peña Rodríguez
15fae4d8f8 fix(ci): move poetry deprecated command to new one (#6384) 2025-01-07 12:38:33 +01:00
dependabot[bot]
3de3fed858 chore(deps): bump next from 14.2.12 to 14.2.22 in /ui (#6356)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 12:29:06 +01:00
dependabot[bot]
1bf4255d93 chore(deps): bump cookie and next-auth in /ui (#5880)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2025-01-07 12:03:45 +01:00
dependabot[bot]
b91a132e61 chore(deps): bump azure-mgmt-compute from 33.0.0 to 33.1.0 (#6219)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 12:02:47 +01:00
dependabot[bot]
39302c9e93 chore(deps): bump botocore from 1.35.88 to 1.35.93 (#6373)
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-01-06 22:59:41 -05:00
dependabot[bot]
65e21c4268 chore(deps): bump trufflesecurity/trufflehog from 3.88.0 to 3.88.1 (#6372)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 18:06:28 -05:00
Rubén De la Torre Vico
3d6a6a9fec fix(aws): add missing sqs service without subservice (#6352) 2025-01-06 12:48:18 -05:00
Rubén De la Torre Vico
d185902c86 docs: add new format CloudFormation for ResourceType in check metadata (#6353)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2025-01-06 10:25:00 -05:00
Prowler Bot
8ce4ad83ed chore(regions_update): Changes in regions for AWS services (#6329)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2025-01-06 10:16:57 -05:00
dependabot[bot]
89620a96bc chore(deps): bump botocore from 1.35.87 to 1.35.88 (#6321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-27 12:21:12 -05:00
dependabot[bot]
f1c008f934 chore(deps-dev): bump coverage from 7.6.9 to 7.6.10 (#6322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-27 09:59:40 -05:00
dependabot[bot]
4d688c9b47 chore(deps): bump boto3 from 1.35.85 to 1.35.87 (#6320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 19:01:01 -05:00
dependabot[bot]
db5481cc9c chore(deps-dev): bump pylint from 3.3.2 to 3.3.3 (#6317)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 13:50:20 -05:00
dependabot[bot]
ce9a5e6484 chore(deps): bump botocore from 1.35.85 to 1.35.87 (#6307)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 09:18:02 -05:00
Víctor Fernández Poyatos
550165b42b feat(compliance): generate compliance reports for GCP scans using API (#6318) 2024-12-26 13:31:20 +01:00
Prowler Bot
080551132a chore(regions_update): Changes in regions for AWS services (#6299)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-23 08:35:48 -05:00
dependabot[bot]
0a61848365 chore(deps): bump boto3 from 1.35.83 to 1.35.85 (#6295)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 07:41:45 -05:00
dependabot[bot]
fcb9ca7795 chore(deps): bump trufflesecurity/trufflehog from 3.87.2 to 3.88.0 (#6298)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-23 07:41:23 -05:00
Víctor Fernández Poyatos
71c58cee9e fix(migrations): fix django migration order dependency (#6302) 2024-12-23 12:26:00 +01:00
Sergio Garcia
c811b6715d fix(gha): run API and UI tests in correct versions (#6294) 2024-12-23 11:47:51 +01:00
Kay Agahd
231829d8cd fix(aws): disallow child-accounts to overwrite policy for ai_services_opt_out (#6229)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2024-12-20 11:04:42 -05:00
dependabot[bot]
dbd2f8becb chore(deps): bump botocore from 1.35.83 to 1.35.85 (#6276)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-20 09:47:05 -05:00
Prowler Bot
cc04e6614e chore(regions_update): Changes in regions for AWS services (#6282)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-20 09:46:55 -05:00
Pablo Lara
a5c5ed614c chore(menu): add API reference link to the sidebar (#6287) 2024-12-20 15:04:29 +01:00
Víctor Fernández Poyatos
ea13241317 fix(users): fix /users/me behavior when having more than 1 users in the same tenant (#6284) 2024-12-20 09:01:23 -05:00
Sergio Garcia
a377a9ff6a chore(gha): solve pypi release github action (#6278) 2024-12-20 08:57:29 -05:00
Víctor Fernández Poyatos
f7e510b333 fix(db-utils): fix batch_delete function (#6283) 2024-12-20 08:55:21 -05:00
Pablo Lara
4472b80f1c chore(findings): remove delta new as filter by default in findings (#6280) 2024-12-20 09:36:01 +01:00
dependabot[bot]
577eb3eec9 chore(deps): bump msgraph-sdk from 1.14.0 to 1.15.0 (#6250)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-20 09:34:46 +01:00
dependabot[bot]
1ed6a1a40f chore(deps): bump trufflesecurity/trufflehog from 3.87.1 to 3.87.2 (#6279)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 17:51:41 -05:00
Sergio Garcia
fe4cd1cddf fix(aws): add missing region to Backup Recovery Point (#6273) 2024-12-19 16:08:22 -05:00
Pablo Lara
6d7a8c8130 feat(roles): RBAC functionality (#6201) 2024-12-19 18:35:10 +01:00
dependabot[bot]
3057aeeacf chore(deps): bump slack-sdk from 3.33.5 to 3.34.0 (#6254)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 12:27:35 -05:00
Sergio Garcia
bb5b63f62f fix(aws): solve None type errors (#6268) 2024-12-19 11:32:33 -05:00
Prowler Bot
58cd944618 chore(regions_update): Changes in regions for AWS services (#6262)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-19 10:05:09 -05:00
Daniel Barranquero
5964b68c86 feat(codeartifact): add new fixer codeartifact_packages_external_public_publishing_disabled_fixer (#6263) 2024-12-19 10:05:01 -05:00
Pepe Fagoaga
c87aaeba04 chore(api): Use prowler ^5.0 (#6266) 2024-12-19 09:40:51 -05:00
dependabot[bot]
6e361005dc chore(deps): bump trufflesecurity/trufflehog from 3.87.0 to 3.87.1 (#6249)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 09:02:44 -05:00
dependabot[bot]
f5ab254bc5 chore(deps): bump microsoft-kiota-abstractions from 1.6.6 to 1.6.7 (#6233)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 09:02:01 -05:00
dependabot[bot]
298392b409 chore(deps): bump google-api-python-client from 2.155.0 to 2.156.0 (#6252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 11:10:10 +01:00
Twodragon
74a2bf0721 feat(prowler-docker): Run Prowler docker with AWS SSO (#5867)
Co-authored-by: twodragon114 <twodragon114@gmail.com>
Co-authored-by: pedrooot <pedromarting3@gmail.com>
2024-12-19 10:55:15 +01:00
dependabot[bot]
ddc5dc0316 chore(deps): bump boto3 from 1.35.81 to 1.35.83 (#6253)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 09:45:16 +01:00
Pepe Fagoaga
d3af947553 fix(gha): make conditional job for checking the repo (#6255) 2024-12-19 14:19:41 +05:45
Pepe Fagoaga
36bb2509ac docs: add note about platform flag in docker (#6256) 2024-12-19 14:18:16 +05:45
Pepe Fagoaga
e4c2b0c2d3 chore: skip action on .env changes (#6257) 2024-12-19 14:17:56 +05:45
Víctor Fernández Poyatos
ac5260ad43 feat(celery): Add configurable broker visibility timeout setting (#6245) 2024-12-19 00:03:11 +05:45
Adrián Jesús Peña Rodríguez
33857109c9 ref(rbac): enable relationship creation when objects is created (#6238) 2024-12-18 16:45:32 +01:00
Pepe Fagoaga
8cc8f76204 fix(.env): remove comment (#6230) 2024-12-18 20:36:03 +05:45
Pedro Martín
8f3229928e chore(config): set default values for empty config fields (#6225) 2024-12-18 09:48:32 -05:00
Pedro Martín
2551992fd8 fix(docs): change typo from provideruid in k8s (#6239)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2024-12-18 09:02:44 -05:00
Prowler Bot
eb1decfce1 chore(regions_update): Changes in regions for AWS services (#6237)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-18 08:51:22 -05:00
Pepe Fagoaga
fd5e7b809f docs: add note about containers arch (#6236) 2024-12-18 11:09:35 +01:00
dependabot[bot]
1ac681226d chore(deps): bump botocore from 1.35.81 to 1.35.83 (#6232)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 18:52:46 -05:00
dependabot[bot]
366940298d chore(deps): bump trufflesecurity/trufflehog from 3.86.1 to 3.87.0 (#6234)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 18:51:32 -05:00
Adrián Jesús Peña Rodríguez
fa400ded7d ref(rbac): improve rbac implementation for views (#6226) 2024-12-17 18:11:48 +01:00
dependabot[bot]
ec9455ff75 chore(deps): bump boto3 from 1.35.80 to 1.35.81 (#6218)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 11:32:30 -05:00
Daniel Barranquero
2183f31ff5 feat(ec2): add new fixers for internet exposed ports (#6223) 2024-12-17 10:04:00 -05:00
Prowler Bot
67257a4212 chore(regions_update): Changes in regions for AWS services (#6222)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-17 10:00:52 -05:00
Pedro Martín
001fa60a11 feat(mutelist): add description field (#6221)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2024-12-17 15:13:55 +01:00
Víctor Fernández Poyatos
0ec3ed8be7 feat(services): Add GET /overviews/services to API (#6029) 2024-12-17 08:47:44 +01:00
dependabot[bot]
3ed0b8a464 chore(deps-dev): bump mkdocs-material from 9.5.48 to 9.5.49 (#6217)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 08:42:55 +01:00
Pedro Martín
fd610d44c0 refactor(gcp): use always <client>.region for checks (#6206) 2024-12-16 18:21:42 -05:00
Adrián Jesús Peña Rodríguez
b8cc4b4f0f feat(stepfunctions): add stepfunctions service and check stepfunctions_statemachine_logging_enabled (#5466)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
Co-authored-by: Rubén De la Torre Vico <rubendltv22@gmail.com>
2024-12-16 11:34:02 -05:00
Pedro Martín
396e51c27d feat(gcp): add service account credentials (#6165) 2024-12-16 10:11:32 -05:00
Daniel Barranquero
36e61cb7a2 feat(ec2): add new fixer ec2_ami_public_fixer (#6177) 2024-12-16 10:09:14 -05:00
Daniel Barranquero
78c6484ddb feat(cloudtrail): add new fixer cloudtrail_logs_s3_bucket_is_not_publicly_accessible_fixer (#6174) 2024-12-16 10:05:34 -05:00
Daniel Barranquero
3f1e90a5b3 feat(s3): add new fixer s3_bucket_policy_public_write_access_fixer (#6173) 2024-12-16 10:01:38 -05:00
dependabot[bot]
e1bfec898f chore(deps): bump botocore from 1.35.80 to 1.35.81 (#6199)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 09:57:03 -05:00
dependabot[bot]
b5b816dac9 chore(deps): bump boto3 from 1.35.79 to 1.35.80 (#6198)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 07:51:44 -05:00
Pepe Fagoaga
57854f23b7 chore(rls): rename tenant_transaction to rls_transaction (#6202) 2024-12-16 12:27:55 +01:00
Rubén De la Torre Vico
9d7499b74f fix(azure): custom Prowler Role for Azure assignableScopes (#6149) 2024-12-16 08:34:17 +01:00
dependabot[bot]
5b0b85c0f8 chore(deps): bump actions/setup-node from 3 to 4 (#5893)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-13 14:57:27 +01:00
Pedro Martín
f7e8df618b chore(labeler): add provider github (#6194) 2024-12-13 09:43:49 -04:00
Adrián Jesús Peña Rodríguez
d00d254c90 feat(api): RBAC system (#6114) 2024-12-13 14:14:40 +01:00
dependabot[bot]
f9fbde6637 chore(deps): bump botocore from 1.35.79 to 1.35.80 (#6172)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-13 13:20:40 +01:00
Sergio Garcia
7b1a0474db fix(aws): set unique resource IDs (#6152) 2024-12-13 13:00:38 +01:00
Pepe Fagoaga
da4f9b8e5f fix(RLS): enforce config security (#6066) 2024-12-13 12:55:09 +01:00
Pepe Fagoaga
32f69d24b6 fix: dependabot syntax (#6181) 2024-12-13 12:20:43 +01:00
Pepe Fagoaga
d032a61a9e chore(dependabot): Add docker (#6180) 2024-12-13 12:13:53 +01:00
dependabot[bot]
07e0dc2ef5 chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /ui (#5881)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2024-12-13 08:25:57 +01:00
dependabot[bot]
9e175e8504 chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /ui (#6110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-13 07:59:50 +01:00
dependabot[bot]
6b8a434cda chore(deps): bump boto3 from 1.35.78 to 1.35.79 (#6171)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-13 07:58:58 +01:00
Pepe Fagoaga
554491a642 chore(gha): build and push OSS UI (#6168) 2024-12-12 19:10:44 +01:00
Pedro Martín
dc4e2f3c85 feat(GHA): build containers for API (#6032)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2024-12-12 19:05:25 +01:00
Daniel Barranquero
7d2c50991b feat(s3): add new fixer s3_bucket_public_access_fixer (#6164)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2024-12-12 12:17:41 -04:00
Pedro Martín
83c204e010 fix(rds): add invalid SG to status_extended (#6157) 2024-12-12 11:51:09 -04:00
dependabot[bot]
316eb049dd chore(deps): bump botocore from 1.35.78 to 1.35.79 (#6153)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 11:29:23 -04:00
Daniel Barranquero
be347b2428 feat(ec2): add new check ec2_launch_template_imdsv2_required (#6139)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2024-12-12 11:27:20 -04:00
Daniel Barranquero
a90c772827 feat(s3): add new fixer s3_bucket_public_list_acl_fixer (#6166) 2024-12-12 11:16:46 -04:00
Daniel Barranquero
26c70976c0 feat(s3): add new fixer s3_bucket_public_write_acl_fixer (#5855) 2024-12-12 11:10:43 -04:00
dependabot[bot]
657310dc25 chore(deps): bump boto3 from 1.35.77 to 1.35.78 (#6154)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 10:39:22 -04:00
Daniel Barranquero
6e595eaf92 feat(ec2): add new fixer ec2_instance_port_cifs_exposed_to_internet_fixer (#6159) 2024-12-12 09:22:56 -04:00
Prowler Bot
997831e33d chore(regions_update): Changes in regions for AWS services (#6158)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-12 09:10:46 -04:00
dependabot[bot]
5920cdc48f chore(deps): bump trufflesecurity/trufflehog from 3.86.0 to 3.86.1 (#6156)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 09:10:20 -04:00
dependabot[bot]
971e73f9cb chore(deps): bump google-api-python-client from 2.154.0 to 2.155.0 (#6155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 09:09:51 -04:00
Mads Brouer Lundholm
bd9673c9de fix(aurora): Add default ports to the check of using non default ports (#5821)
Co-authored-by: Mads Rantala Lundholm <mao@bankdata.dk>
Co-authored-by: Sergio Garcia <sergargar1@gmail.com>
2024-12-11 13:01:45 -04:00
johannes-engler-mw
eded97d735 feat(azure): check for minimal TLS version for Azure SQL server (#5745)
Co-authored-by: Rubén De la Torre Vico <ruben@prowler.com>
2024-12-11 16:37:53 +01:00
Daniel Barranquero
fdb1956b0b feat(opensearch): add new fixer opensearch_service_domains_not_publicly_accessible_fixer (#5926) 2024-12-11 11:29:48 -04:00
Daniel Barranquero
a915c04e9e fix(autoscaling): autoscaling_group_launch_configuration_requires_imdsv2 fails if Launch Template is used (#6111)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2024-12-11 11:18:30 -04:00
Daniel Barranquero
07178ac69a feat(glacier): add new fixer glacier_vaults_policy_public_access_fixer (#5950) 2024-12-11 11:10:12 -04:00
Daniel Barranquero
9b434d4856 feat(ecr): add new fixer ecr_repositories_not_publicly_accessible_fixer (#5923) 2024-12-11 10:42:11 -04:00
dependabot[bot]
0758e97628 chore(deps): bump botocore from 1.35.77 to 1.35.78 (#6132)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 10:19:37 -04:00
Sergio Garcia
b486007f95 fix(README): show latest release (#6145) 2024-12-11 10:19:06 -04:00
dependabot[bot]
0c0887afef chore(deps): bump trufflesecurity/trufflehog from 3.85.0 to 3.86.0 (#6130)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 09:29:14 -04:00
dependabot[bot]
805ed81031 chore(deps): bump boto3 from 1.35.76 to 1.35.77 (#6131)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 09:26:07 -04:00
Prowler Bot
ec3fddf5b1 chore(regions_update): Changes in regions for AWS services (#6136)
Co-authored-by: MrCloudSec <38561120+MrCloudSec@users.noreply.github.com>
2024-12-11 09:25:17 -04:00
Rubén De la Torre Vico
d7b0bc02ba feat(app): add support for TLS 1.3 to Web Apps check (#6004) 2024-12-11 13:14:29 +01:00
Pablo Lara
4d1c8eae8f feat(users): user detail can be edited now properly (#6135) 2024-12-11 10:05:30 +01:00
Sergio Garcia
989ccf4ae3 fix(iam): set unique resource id for each user access key (#6128) 2024-12-11 09:13:49 +01:00
Pedro Martín
9c089756c3 fix(compliance_tables): add correct values for findings (#6122)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2024-12-10 15:40:45 -04:00
Hugo Pereira Brito
8d4b0914a8 fix(aws): get firewall manager managed rule groups (#6119) 2024-12-10 15:34:22 -04:00
Hugo Pereira Brito
1ae3f89aab fix(aws): check AWS Owned keys in firehose_stream_encrypted_at_rest (#6108) 2024-12-10 13:42:13 -04:00
Daniel Barranquero
b984f0423a feat(sqs): add new fixer sqs_queues_not_publicly_accessible_fixer (#5911)
Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
2024-12-10 12:26:42 -04:00
Sergio Garcia
f2f196cfcd fix(aws): set IAM identity as resource in threat detection (#6048) 2024-12-10 17:03:01 +01:00
dependabot[bot]
6471d936bb chore(deps): bump msgraph-sdk from 1.12.0 to 1.14.0 (#5957)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 11:42:40 -04:00
Adrián Jesús Peña Rodríguez
21bbdccc41 fix(deploy): temporal fix for the alpine-python segmentation fault (#6109) 2024-12-10 16:27:52 +01:00
Sergio Garcia
48946fa4f7 fix(gcp): make sure default project is active (#6097) 2024-12-10 11:06:48 -04:00
dependabot[bot]
9312dda7c2 chore(deps): bump microsoft-kiota-abstractions from 1.6.2 to 1.6.6 (#6038)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 10:37:04 -04:00
dependabot[bot]
e3013329ee chore(deps): bump botocore from 1.35.76 to 1.35.77 (#6098)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 09:26:36 -04:00
Sergio Garcia
38a0d2d740 fix(aws): set same severity for EC2 IMDSv2 checks (#6046) 2024-12-10 08:55:41 +01:00
Mario Rodriguez Lopez
5c2adf1e14 docs(unitesting): Make some fixes to the documentation (#6102) 2024-12-10 08:51:19 +01:00
Daniel Barranquero
7ddd2c04c8 feat(awslambda): add new fixer awslambda_function_not_publicly_accessible_fixer (#5840) 2024-12-09 12:28:42 -04:00
Pepe Fagoaga
9a55632d8e fix(backport): more than one backport tag is allowed (#6090) 2024-12-09 17:19:33 +01:00
dependabot[bot]
f8b4427505 chore(deps-dev): bump vulture from 2.13 to 2.14 (#6068)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 11:10:41 -04:00
Sergio Garcia
f1efc1456d chore(dependabot): change interval of PRs (#6086) 2024-12-09 15:46:28 +01:00
Sergio Garcia
2ea5851b67 docs(api): add commands to run API scheduler (#6085) 2024-12-09 10:34:02 -04:00
dependabot[bot]
a3051bc4e3 chore(deps-dev): bump mkdocs-material from 9.5.47 to 9.5.48 (#6073)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 10:14:08 -04:00
Pepe Fagoaga
d454427b8b fix(backport): remove v from branch prefix (#6081) 2024-12-09 10:13:20 -04:00
Pepe Fagoaga
4b41bd6adf chore(containers): support for v4.6 branch (#6063)
Co-authored-by: MrCloudSec <hello@mistercloudsec.com>
2024-12-09 09:23:06 -04:00
Pepe Fagoaga
cdd044d120 chore(dependabot): Update for UI and v4 (#6062) 2024-12-09 09:15:03 -04:00
Pepe Fagoaga
213a793fbc chore(actions): standardize names (#6059) 2024-12-09 09:14:06 -04:00
Pepe Fagoaga
a8a567c588 docs: Prowler SaaS -> Cloud and add missing compliance (#6061) 2024-12-09 09:12:54 -04:00
Pepe Fagoaga
fefe89a1ed fix(backport): Add action to detect labels (#5270) 2024-12-09 09:12:08 -04:00
Sergio Garcia
493fe2d523 docs(env): move warning about env files (#6049) 2024-12-09 11:11:05 +01:00
dependabot[bot]
d8fc830f1d chore(deps): bump boto3 from 1.35.71 to 1.35.76 (#6054)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-09 10:11:51 +01:00
Pepe Fagoaga
b6c3ba0f0d chore: delete unneeded requirements file (#6056) 2024-12-09 09:07:10 +01:00
dependabot[bot]
32cd39d158 chore(deps-dev): bump coverage from 7.6.8 to 7.6.9 (#6053)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 20:29:06 -04:00
dependabot[bot]
203275817f chore(deps-dev): bump pytest from 8.3.3 to 8.3.4 (#5992)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 12:53:11 -04:00
dependabot[bot]
c05c3396b5 chore(deps-dev): bump mkdocs-material from 9.5.46 to 9.5.47 (#5988)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 11:56:37 -04:00
dependabot[bot]
8f172aec8a chore(deps-dev): bump pylint from 3.3.1 to 3.3.2 (#5993)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 11:04:28 -04:00
dependabot[bot]
263a7e2134 chore(deps): bump botocore from 1.35.71 to 1.35.76 (#6037) 2024-12-06 09:41:57 -04:00
dependabot[bot]
a2ea216604 chore(deps): bump slack-sdk from 3.33.4 to 3.33.5 (#6039)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 08:44:00 -04:00
dependabot[bot]
77c572f990 chore(deps): bump trufflesecurity/trufflehog from 3.84.1 to 3.85.0 (#6040)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 08:38:14 -04:00
Prowler Bot
bb0c346c4d chore(regions_update): Changes in regions for AWS services (#6041)
Co-authored-by: sergargar <38561120+sergargar@users.noreply.github.com>
2024-12-06 08:38:03 -04:00
Daniel Barranquero
2ce8e1fd21 fix(backup): modify list recovery points call (#5996) 2024-12-06 08:35:29 -04:00
Pepe Fagoaga
ecfd94aeb1 fix(codecov): create components (#6028) 2024-12-05 16:35:56 +01:00
Pedro Martín
eddc672264 chore(version): update prowler version (#6027) 2024-12-05 13:51:13 +01:00
Pedro Martín
8c71a39487 docs(prowler-app): add link to https://api.prowler.com/api/v1/docs (#6016) 2024-12-05 11:01:51 +01:00
Pedro Martín
ff0ac27723 docs(index): update index with images (#6015) 2024-12-05 11:01:42 +01:00
Víctor Fernández Poyatos
ad7134d283 fix(tenant): fix delete tenants behavior (#6013) 2024-12-04 13:57:16 +01:00
Pablo Lara
58723ae52e fix(invitations): remove wrong url (#6005) 2024-12-03 21:08:31 +01:00
1689 changed files with 57864 additions and 15334 deletions

30
.env
View File

@@ -6,13 +6,14 @@
PROWLER_UI_VERSION="latest"
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
AUTH_TRUST_HOST=true
UI_PORT=3000
# openssl rand -base64 32
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
#### Prowler API Configuration ####
PROWLER_API_VERSION="latest"
PROWLER_API_VERSION="stable"
# PostgreSQL settings
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
POSTGRES_HOST=postgres-db
@@ -29,6 +30,23 @@ VALKEY_HOST=valkey
VALKEY_PORT=6379
VALKEY_DB=0
# API scan settings
# The AWS access key to be used when uploading scan artifacts to an S3 bucket
# If left empty, default AWS credentials resolution behavior will be used
ARTIFACTS_AWS_ACCESS_KEY_ID=""
# The AWS secret key to be used when uploading scan artifacts to an S3 bucket
ARTIFACTS_AWS_SECRET_ACCESS_KEY=""
# An optional AWS session token
ARTIFACTS_AWS_SESSION_TOKEN=""
# The AWS region where your S3 bucket is located (e.g., "us-east-1")
ARTIFACTS_AWS_DEFAULT_REGION=""
# The name of the S3 bucket where scan artifacts should be stored
ARTIFACTS_AWS_S3_OUTPUT_BUCKET=""
# Django settings
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
DJANGO_BIND_ADDRESS=0.0.0.0
@@ -40,9 +58,12 @@ DJANGO_LOGGING_FORMATTER=human_readable
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
# Applies to both Django and Celery Workers
DJANGO_LOGGING_LEVEL=INFO
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
DJANGO_ACCESS_TOKEN_LIFETIME=30 # Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME=1440 # Token lifetime is in minutes
# Defaults to the maximum available based on CPU cores if not set.
DJANGO_WORKERS=4
# Token lifetime is in minutes
DJANGO_ACCESS_TOKEN_LIFETIME=30
# Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME=1440
DJANGO_CACHE_MAX_AGE=3600
DJANGO_STALE_WHILE_REVALIDATE=60
DJANGO_MANAGE_DB_PARTITIONS=True
@@ -87,3 +108,4 @@ jQIDAQAB
-----END PUBLIC KEY-----"
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400

View File

@@ -1,3 +1,3 @@
name: "Custom CodeQL Config for API"
name: "API - CodeQL Config"
paths:
- 'api/'
- "api/"

View File

@@ -1,4 +0,0 @@
name: "Custom CodeQL Config"
paths-ignore:
- 'api/'
- 'ui/'

4
.github/codeql/sdk-codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
name: "SDK - CodeQL Config"
paths-ignore:
- "api/"
- "ui/"

View File

@@ -1,3 +1,3 @@
name: "Custom CodeQL Config for UI"
name: "UI - CodeQL Config"
paths:
- "ui/"

View File

@@ -5,6 +5,7 @@
version: 2
updates:
# v5
- package-ecosystem: "pip"
directory: "/"
schedule:
@@ -14,6 +15,18 @@ updates:
labels:
- "dependencies"
- "pip"
- package-ecosystem: "pip"
directory: "/api"
schedule:
interval: "daily"
open-pull-requests-limit: 10
target-branch: master
labels:
- "dependencies"
- "pip"
- "component/api"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
@@ -24,20 +37,77 @@ updates:
- "dependencies"
- "github_actions"
- package-ecosystem: "npm"
directory: "/ui"
schedule:
interval: "daily"
open-pull-requests-limit: 10
target-branch: master
labels:
- "dependencies"
- "npm"
- "component/ui"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: master
labels:
- "dependencies"
- "docker"
# v4.6
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
open-pull-requests-limit: 10
target-branch: v4.6
labels:
- "dependencies"
- "pip"
- "v4"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: v4.6
labels:
- "dependencies"
- "github_actions"
- "v4"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
target-branch: v4.6
labels:
- "dependencies"
- "docker"
- "v4"
# v3
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
target-branch: v3
labels:
- "dependencies"
- "pip"
- "v3"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "monthly"
open-pull-requests-limit: 10
target-branch: v3
labels:

5
.github/labeler.yml vendored
View File

@@ -22,6 +22,11 @@ provider/kubernetes:
- any-glob-to-any-file: "prowler/providers/kubernetes/**"
- any-glob-to-any-file: "tests/providers/kubernetes/**"
provider/github:
- changed-files:
- any-glob-to-any-file: "prowler/providers/github/**"
- any-glob-to-any-file: "tests/providers/github/**"
github_actions:
- changed-files:
- any-glob-to-any-file: ".github/workflows/*"

View File

@@ -15,7 +15,8 @@ Please include a summary of the change and which issue is fixed. List any depend
- [ ] Review if the code is being covered by tests.
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
- [ ] Review if backport is needed.
- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md)
### License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

View File

@@ -0,0 +1,98 @@
name: API - Build and Push containers
on:
push:
branches:
- "master"
paths:
- "api/**"
- ".github/workflows/api-build-lint-push-containers.yml"
# Uncomment the code below to test this action on PRs
# pull_request:
# branches:
# - "master"
# paths:
# - "api/**"
# - ".github/workflows/api-build-lint-push-containers.yml"
release:
types: [published]
env:
# Tags
LATEST_TAG: latest
RELEASE_TAG: ${{ github.event.release.tag_name }}
STABLE_TAG: stable
WORKING_DIRECTORY: ./api
# Container Registries
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-api
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
# Build Prowler OSS container
container-build-push:
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIRECTORY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push container image (latest)
# Comment the following line for testing
if: github.event_name == 'push'
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
# Set push: false for testing
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push container image (release)
if: github.event_name == 'release'
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
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

View File

@@ -9,22 +9,18 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "API - CodeQL"
name: API - CodeQL
on:
push:
branches:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths:
- "api/**"
pull_request:
branches:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths:
- "api/**"

View File

@@ -1,14 +1,16 @@
name: "API - Pull Request"
name: API - Pull Request
on:
push:
branches:
- "master"
- "v5.*"
paths:
- "api/**"
pull_request:
branches:
- "master"
- "v5.*"
paths:
- "api/**"
@@ -69,6 +71,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@v45
@@ -80,18 +83,21 @@ jobs:
api/permissions/**
api/README.md
api/mkdocs.yml
- name: Install poetry
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
python -m pip install --upgrade pip
pipx install poetry
pipx install poetry==1.8.5
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
@@ -108,49 +114,60 @@ jobs:
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry lock --check
poetry check --lock
- name: Lint with ruff
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run ruff check . --exclude contrib
- name: Check Format with ruff
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run ruff format --check . --exclude contrib
- name: Lint with pylint
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/
- name: Bandit
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run bandit -q -lll -x '*_test.py,./contrib/' -r .
- name: Safety
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run safety check --ignore 70612,66963
- name: Vulture
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 .
- name: Hadolint
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
/tmp/hadolint Dockerfile --ignore=DL3013
- name: Test with pytest
working-directory: ./api
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run pytest --cov=./src/backend --cov-report=xml src/backend
- name: Upload coverage reports to Codecov
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: api

View File

@@ -1,42 +1,47 @@
name: Automatic Backport
name: Prowler - Automatic Backport
on:
pull_request_target:
branches: ['master']
types: ['labeled', 'closed']
env:
# The prefix of the label that triggers the backport must not contain the branch name
# so, for example, if the branch is 'master', the label should be 'backport-to-<branch>'
BACKPORT_LABEL_PREFIX: backport-to-
BACKPORT_LABEL_IGNORE: was-backported
jobs:
backport:
name: Backport PR
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport'))
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport')) && !(contains(github.event.pull_request.labels.*.name, 'was-backported'))
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
contents: write
steps:
# Workaround not to fail the workflow if the PR does not need a backport
# https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2258561266
- name: Check for backport labels
id: check_labels
run: |-
labels='${{ toJSON(github.event.pull_request.labels.*.name) }}'
echo "$labels"
matched=$(echo "${labels}" | jq '. | map(select(startswith("backport-to-"))) | length')
echo "matched=$matched"
echo "matched=$matched" >> $GITHUB_OUTPUT
- name: Check labels
id: preview_label_check
uses: docker://agilepathway/pull-request-label-checker:v1.6.55
with:
allow_failure: true
prefix_mode: true
any_of: ${{ env.BACKPORT_LABEL_PREFIX }}
none_of: ${{ env.BACKPORT_LABEL_IGNORE }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
- name: Backport Action
if: fromJSON(steps.check_labels.outputs.matched) > 0
if: steps.preview_label_check.outputs.label_check == 'success'
uses: sorenlouv/backport-github-action@v9.5.1
with:
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
auto_backport_label_prefix: backport-to-
auto_backport_label_prefix: ${{ env.BACKPORT_LABEL_PREFIX }}
- name: Info log
if: ${{ success() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
if: ${{ success() && steps.preview_label_check.outputs.label_check == 'success' }}
run: cat ~/.backport/backport.info.log
- name: Debug log
if: ${{ failure() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
if: ${{ failure() && steps.preview_label_check.outputs.label_check == 'success' }}
run: cat ~/.backport/backport.debug.log

View File

@@ -1,4 +1,4 @@
name: Pull Request Documentation Link
name: Prowler - Pull Request Documentation Link
on:
pull_request:

View File

@@ -1,4 +1,4 @@
name: Find secrets
name: Prowler - Find secrets
on: pull_request
@@ -11,9 +11,9 @@ jobs:
with:
fetch-depth: 0
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@v3.84.1
uses: trufflesecurity/trufflehog@v3.88.4
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --only-verified
extra_args: --only-verified

View File

@@ -1,4 +1,4 @@
name: "Pull Request Labeler"
name: Prowler - PR Labeler
on:
pull_request_target:

View File

@@ -1,9 +1,13 @@
name: Build and Push containers
name: SDK - Build and Push containers
on:
push:
branches:
# For `v3-latest`
- "v3"
# For `v4-latest`
- "v4.6"
# For `latest`
- "master"
paths-ignore:
- ".github/**"
@@ -64,7 +68,7 @@ jobs:
- name: Install Poetry
run: |
pipx install poetry
pipx install poetry==1.8.5
pipx inject poetry poetry-bumpversion
- name: Get Prowler version
@@ -85,8 +89,8 @@ jobs:
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
;;
4)
4)
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
;;

View File

@@ -9,22 +9,24 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: SDK - CodeQL
on:
push:
branches:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths-ignore:
- 'ui/**'
- 'api/**'
pull_request:
branches:
branches:
- "master"
- "v3"
- "v4.*"
- "v5.*"
paths-ignore:
- 'ui/**'
- 'api/**'
@@ -55,7 +57,7 @@ jobs:
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
config-file: ./.github/codeql/sdk-codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

View File

@@ -1,4 +1,4 @@
name: "Pull Request"
name: SDK - Pull Request
on:
push:
@@ -22,6 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Test if changes are in not ignored paths
id: are-non-ignored-files-changed
uses: tj-actions/changed-files@v45
@@ -36,17 +37,22 @@ jobs:
README.md
mkdocs.yml
.backportrc.json
.env
docker-compose*
- name: Install poetry
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
python -m pip install --upgrade pip
pipx install poetry
pipx install poetry==1.8.5
- name: Set up Python ${{ matrix.python-version }}
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install dependencies
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
@@ -57,44 +63,56 @@ jobs:
sed -E 's/.*"v([^"]+)".*/\1/' \
) && curl -L -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64" \
&& chmod +x /tmp/hadolint
- name: Poetry check
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry lock --check
poetry check --lock
- name: Lint with flake8
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api
- name: Checking format with black
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run black --exclude api ui --check .
- name: Lint with pylint
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/
- name: Bandit
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r .
- name: Safety
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run safety check --ignore 70612 -r pyproject.toml
- name: Vulture
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
- name: Hadolint
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
/tmp/hadolint Dockerfile --ignore=DL3013
- name: Test with pytest
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
run: |
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
- name: Upload coverage reports to Codecov
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
flags: prowler

View File

@@ -1,4 +1,4 @@
name: PyPI release
name: SDK - PyPI release
on:
release:
@@ -10,12 +10,40 @@ env:
CACHE: "poetry"
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
release-prowler-job:
runs-on: ubuntu-latest
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
env:
POETRY_VIRTUALENVS_CREATE: "false"
name: Release Prowler to PyPI
steps:
- name: Repository check
working-directory: /tmp
run: |
if [[ "${{ github.repository }}" != "prowler-cloud/prowler" ]]; then
echo "This action only runs for prowler-cloud/prowler"
exit 1
fi
- name: Get Prowler version
run: |
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
@@ -40,7 +68,7 @@ jobs:
- name: Install dependencies
run: |
pipx install poetry
pipx install poetry==1.8.5
- name: Setup Python
uses: actions/setup-python@v5

View File

@@ -1,6 +1,6 @@
# This is a basic workflow to help you get started with Actions
name: Refresh regions of AWS services
name: SDK - Refresh AWS services' regions
on:
schedule:

View File

@@ -0,0 +1,98 @@
name: UI - Build and Push containers
on:
push:
branches:
- "master"
paths:
- "ui/**"
- ".github/workflows/ui-build-lint-push-containers.yml"
# Uncomment the below code to test this action on PRs
# pull_request:
# branches:
# - "master"
# paths:
# - "ui/**"
# - ".github/workflows/ui-build-lint-push-containers.yml"
release:
types: [published]
env:
# Tags
LATEST_TAG: latest
RELEASE_TAG: ${{ github.event.release.tag_name }}
STABLE_TAG: stable
WORKING_DIRECTORY: ./ui
# Container Registries
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler-ui
jobs:
repository-check:
name: Repository check
runs-on: ubuntu-latest
outputs:
is_repo: ${{ steps.repository_check.outputs.is_repo }}
steps:
- name: Repository check
id: repository_check
working-directory: /tmp
run: |
if [[ ${{ github.repository }} == "prowler-cloud/prowler" ]]
then
echo "is_repo=true" >> "${GITHUB_OUTPUT}"
else
echo "This action only runs for prowler-cloud/prowler"
echo "is_repo=false" >> "${GITHUB_OUTPUT}"
fi
# Build Prowler OSS container
container-build-push:
needs: repository-check
if: needs.repository-check.outputs.is_repo == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ env.WORKING_DIRECTORY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push container image (latest)
# Comment the following line for testing
if: github.event_name == 'push'
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
# Set push: false for testing
push: true
tags: |
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push container image (release)
if: github.event_name == 'release'
uses: docker/build-push-action@v6
with:
context: ${{ env.WORKING_DIRECTORY }}
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

View File

@@ -9,20 +9,18 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "UI - CodeQL"
name: UI - CodeQL
on:
push:
branches:
- "master"
- "v4.*"
- "v5.*"
paths:
- "ui/**"
pull_request:
branches:
- "master"
- "v4.*"
- "v5.*"
paths:
- "ui/**"

View File

@@ -1,9 +1,16 @@
name: "UI - Pull Request"
name: UI - Pull Request
on:
push:
branches:
- "master"
- "v5.*"
paths:
- "ui/**"
pull_request:
branches:
- master
- "v5.*"
paths:
- 'ui/**'
@@ -20,7 +27,7 @@ jobs:
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
@@ -31,4 +38,4 @@ jobs:
run: npm run healthcheck
- name: Build the application
working-directory: ./ui
run: npm run build
run: npm run build

1
.gitignore vendored
View File

@@ -45,6 +45,7 @@ junit-reports/
# Terraform
.terraform*
*.tfstate
*.tfstate.*
# .env
ui/.env*

View File

@@ -27,6 +27,7 @@ repos:
hooks:
- id: shellcheck
exclude: contrib
## PYTHON
- repo: https://github.com/myint/autoflake
rev: v2.3.1
@@ -61,8 +62,25 @@ repos:
rev: 1.8.0
hooks:
- id: poetry-check
name: API - poetry-check
args: ["--directory=./api"]
pass_filenames: false
- id: poetry-lock
args: ["--no-update"]
name: API - poetry-lock
args: ["--no-update", "--directory=./api"]
pass_filenames: false
- id: poetry-check
name: SDK - poetry-check
args: ["--directory=./"]
pass_filenames: false
- id: poetry-lock
name: SDK - poetry-lock
args: ["--no-update", "--directory=./"]
pass_filenames: false
- repo: https://github.com/hadolint/hadolint
rev: v2.13.0-beta
@@ -90,7 +108,7 @@ repos:
- id: bandit
name: bandit
description: "Bandit is a tool for finding common security issues in Python code"
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/' -r .'
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
language: system
files: '.*\.py'
@@ -103,7 +121,6 @@ repos:
- id: vulture
name: vulture
description: "Vulture finds unused code in Python programs."
entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .'
exclude: 'api/src/backend/'
entry: bash -c 'vulture --exclude "contrib,.venv,api/src/backend/api/tests/,api/src/backend/conftest.py,api/src/backend/tasks/tests/" --min-confidence 100 .'
language: system
files: '.*\.py'

View File

@@ -1,4 +1,4 @@
FROM python:3.12-alpine
FROM python:3.12.8-alpine3.20
LABEL maintainer="https://github.com/prowler-cloud/prowler"

View File

@@ -3,7 +3,7 @@
<img align="center" src="https://github.com/prowler-cloud/prowler/blob/master/docs/img/prowler-logo-white.png#gh-dark-mode-only" width="50%" height="50%">
</p>
<p align="center">
<b><i>Prowler SaaS </b> and <b>Prowler Open Source</b> are as dynamic and adaptable as the environment theyre meant to protect. Trusted by the leaders in security.
<b><i>Prowler Open Source</b> is as dynamic and adaptable as the environment theyre meant to protect. Trusted by the leaders in security.
</p>
<p align="center">
<b>Learn more at <a href="https://prowler.com">prowler.com</i></b>
@@ -29,7 +29,7 @@
<p align="center">
<a href="https://github.com/prowler-cloud/prowler"><img alt="Repo size" src="https://img.shields.io/github/repo-size/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/issues"><img alt="Issues" src="https://img.shields.io/github/issues/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler?include_prereleases"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/v/release/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler/releases"><img alt="Version" src="https://img.shields.io/github/release-date/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler"><img alt="Contributors" src="https://img.shields.io/github/contributors-anon/prowler-cloud/prowler"></a>
<a href="https://github.com/prowler-cloud/prowler"><img alt="License" src="https://img.shields.io/github/license/prowler-cloud/prowler"></a>
@@ -43,7 +43,7 @@
# Description
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler SaaS</a>.
**Prowler** is an Open Source security tool to perform AWS, Azure, Google Cloud and Kubernetes security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness, and also remediations! We have Prowler CLI (Command Line Interface) that we call Prowler Open Source and a service on top of it that we call <a href="https://prowler.com">Prowler Cloud</a>.
## Prowler App
@@ -71,10 +71,12 @@ 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` | 3 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`|
| Azure | 139 | 18 -> `prowler azure --list-services` | 4 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` |
| Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` |
| AWS | 564 | 82 | 30 | 10 |
| GCP | 77 | 13 | 4 | 3 |
| Azure | 140 | 18 | 5 | 3 |
| Kubernetes | 83 | 7 | 2 | 7 |
> 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
@@ -98,6 +100,7 @@ curl -LO https://raw.githubusercontent.com/prowler-cloud/prowler/refs/heads/mast
docker compose up -d
```
> Containers are built for `linux/amd64`. If your workstation's architecture is different, please set `DOCKER_DEFAULT_PLATFORM=linux/amd64` in your environment or use the `--platform linux/amd64` flag in the docker command.
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
### From GitHub
@@ -139,6 +142,19 @@ cd src/backend
python -m celery -A config.celery worker -l info -E
```
**Commands to run the API Scheduler**
``` console
git clone https://github.com/prowler-cloud/prowler
cd prowler/api
poetry install
poetry shell
set -a
source .env
cd src/backend
python -m celery -A config.celery beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
```
**Commands to run the UI**
``` console

View File

@@ -22,6 +22,7 @@ DJANGO_SECRETS_ENCRYPTION_KEY=""
# Decide whether to allow Django manage database table partitions
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
# PostgreSQL settings
# If running django and celery on host, use 'localhost', else use 'postgres-db'

View File

@@ -1,4 +1,4 @@
FROM python:3.12-alpine AS build
FROM python:3.12.8-alpine3.20 AS build
LABEL maintainer="https://github.com/prowler-cloud/api"

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 -E --max-tasks-per-child 1
}
start_worker_beat() {

2312
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,11 @@ description = "Prowler's API (Django/DRF)"
license = "Apache-2.0"
name = "prowler-api"
package-mode = false
version = "1.0.0"
version = "1.4.0"
[tool.poetry.dependencies]
celery = {extras = ["pytest"], version = "^5.4.0"}
django = "5.1.1"
django = "5.1.5"
django-celery-beat = "^2.7.0"
django-celery-results = "^2.5.1"
django-cors-headers = "4.4.0"
@@ -37,6 +37,7 @@ 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"
@@ -48,8 +49,8 @@ pytest-env = "1.1.3"
pytest-randomly = "3.15.0"
pytest-xdist = "3.6.1"
ruff = "0.5.0"
safety = "3.2.3"
vulture = "2.11"
safety = "3.2.9"
vulture = "2.14"
[tool.poetry.scripts]
celery = "src.backend.config.settings.celery"

View File

@@ -1,20 +1,23 @@
import uuid
from django.db import transaction, connection
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from rest_framework import permissions
from rest_framework.exceptions import NotAuthenticated
from rest_framework.filters import SearchFilter
from rest_framework_json_api import filters
from rest_framework_json_api.serializers import ValidationError
from rest_framework_json_api.views import ModelViewSet
from rest_framework_simplejwt.authentication import JWTAuthentication
from api.db_router import MainRouter
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
from api.filters import CustomDjangoFilterBackend
from api.models import Role, Tenant
from api.rbac.permissions import HasPermissions
class BaseViewSet(ModelViewSet):
authentication_classes = [JWTAuthentication]
permission_classes = [permissions.IsAuthenticated]
required_permissions = []
permission_classes = [permissions.IsAuthenticated, HasPermissions]
filter_backends = [
filters.QueryParameterValidationFilter,
filters.OrderingFilter,
@@ -28,6 +31,17 @@ class BaseViewSet(ModelViewSet):
ordering_fields = "__all__"
ordering = ["id"]
def initial(self, request, *args, **kwargs):
"""
Sets required_permissions before permissions are checked.
"""
self.set_required_permissions()
super().initial(request, *args, **kwargs)
def set_required_permissions(self):
"""This is an abstract method that must be implemented by subclasses."""
NotImplemented
def get_queryset(self):
raise NotImplementedError
@@ -47,13 +61,7 @@ class BaseRLSViewSet(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
with rls_transaction(tenant_id):
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
@@ -66,13 +74,60 @@ class BaseRLSViewSet(BaseViewSet):
class BaseTenantViewset(BaseViewSet):
def dispatch(self, request, *args, **kwargs):
with transaction.atomic():
return super().dispatch(request, *args, **kwargs)
tenant = super().dispatch(request, *args, **kwargs)
try:
# If the request is a POST, create the admin role
if request.method == "POST":
isinstance(tenant, dict) and self._create_admin_role(tenant.data["id"])
except Exception as e:
self._handle_creation_error(e, tenant)
raise
return tenant
def _create_admin_role(self, tenant_id):
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,
)
def _handle_creation_error(self, error, tenant):
if tenant.data.get("id"):
try:
Tenant.objects.using(MainRouter.admin_db).filter(
id=tenant.data["id"]
).delete()
except ObjectDoesNotExist:
pass # Tenant might not exist, handle gracefully
def initial(self, request, *args, **kwargs):
user_id = str(request.user.id)
if (
request.resolver_match.url_name != "tenant-detail"
and request.method != "DELETE"
):
user_id = str(request.user.id)
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.user_id', '{user_id}', TRUE);")
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
return super().initial(request, *args, **kwargs)
# TODO: DRY this when we have time
if request.auth is None:
raise NotAuthenticated
tenant_id = request.auth.get("tenant_id")
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
with rls_transaction(tenant_id):
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)
@@ -92,12 +147,6 @@ class BaseUserViewset(BaseViewSet):
if tenant_id is None:
raise NotAuthenticated("Tenant ID is not present in token")
try:
uuid.UUID(tenant_id)
except ValueError:
raise ValidationError("Tenant ID must be a valid UUID")
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
with rls_transaction(tenant_id):
self.request.tenant_id = tenant_id
return super().initial(request, *args, **kwargs)

View File

@@ -4,13 +4,17 @@ class MainRouter:
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 model_table_name.startswith(
"silk_"
):
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 model_table_name.startswith("django_") or model_table_name.startswith(
"silk_"
):
return self.admin_db
return None

View File

@@ -1,13 +1,14 @@
import secrets
import uuid
from contextlib import contextmanager
from datetime import datetime, timedelta, timezone
from django.conf import settings
from django.contrib.auth.models import BaseUserManager
from django.core.paginator import Paginator
from django.db import connection, models, transaction
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"
DB_PASSWORD = (
@@ -23,6 +24,8 @@ TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
POSTGRES_TENANT_VAR = "api.tenant_id"
POSTGRES_USER_VAR = "api.user_id"
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
@contextmanager
def psycopg_connection(database_alias: str):
@@ -44,10 +47,23 @@ def psycopg_connection(database_alias: str):
@contextmanager
def tenant_transaction(tenant_id: str):
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
"""
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
if the value is a valid UUID.
Args:
value (str): Database configuration parameter value.
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
"""
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
try:
# just in case the value is an UUID object
uuid.UUID(str(value))
except ValueError:
raise ValidationError("Must be a valid UUID")
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
yield cursor
@@ -103,15 +119,18 @@ def batch_delete(queryset, batch_size=5000):
total_deleted = 0
deletion_summary = {}
paginator = Paginator(queryset.order_by("id").only("id"), batch_size)
for page_num in paginator.page_range:
batch_ids = [obj.id for obj in paginator.page(page_num).object_list]
while True:
# Get a batch of IDs to delete
batch_ids = set(
queryset.values_list("id", flat=True).order_by("id")[:batch_size]
)
if not batch_ids:
# No more objects to delete
break
deleted_count, deleted_info = queryset.filter(id__in=batch_ids).delete()
total_deleted += deleted_count
for model_label, count in deleted_info.items():
deletion_summary[model_label] = deletion_summary.get(model_label, 0) + count

View File

@@ -1,6 +1,10 @@
import uuid
from functools import wraps
from django.db import connection, transaction
from rest_framework_json_api.serializers import ValidationError
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
def set_tenant(func):
@@ -31,7 +35,7 @@ def set_tenant(func):
pass
# When calling the task
some_task.delay(arg1, tenant_id="1234-abcd-5678")
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
# The tenant context will be set before the task logic executes.
"""
@@ -43,9 +47,12 @@ def set_tenant(func):
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(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
return func(*args, **kwargs)

View File

@@ -26,11 +26,13 @@ from api.models import (
Finding,
Invitation,
Membership,
PermissionChoices,
Provider,
ProviderGroup,
ProviderSecret,
Resource,
ResourceTag,
Role,
Scan,
ScanSummary,
SeverityChoices,
@@ -317,6 +319,28 @@ class FindingFilter(FilterSet):
field_name="resources__type", lookup_expr="icontains"
)
# 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")
@@ -424,6 +448,16 @@ class FindingFilter(FilterSet):
return queryset.filter(id__lte=end).filter(inserted_at__lte=value)
def filter_resource_tag(self, queryset, name, value):
overall_query = Q()
for key_value_pair in value:
tag_key, tag_value = key_value_pair.split(":", 1)
overall_query |= Q(
resources__tags__key__icontains=tag_key,
resources__tags__value__icontains=tag_value,
)
return queryset.filter(overall_query).distinct()
@staticmethod
def maybe_date_to_datetime(value):
dt = value
@@ -481,6 +515,26 @@ class UserFilter(FilterSet):
}
class RoleFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
permission_state = ChoiceFilter(
choices=PermissionChoices.choices, method="filter_permission_state"
)
def filter_permission_state(self, queryset, name, value):
return Role.filter_by_permission_state(queryset, value)
class Meta:
model = Role
fields = {
"id": ["exact", "in"],
"name": ["exact", "in"],
"inserted_at": ["gte", "lte"],
"updated_at": ["gte", "lte"],
}
class ComplianceOverviewFilter(FilterSet):
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
provider_type = ChoiceFilter(choices=Provider.ProviderChoices.choices)
@@ -521,3 +575,25 @@ class ScanSummaryFilter(FilterSet):
"inserted_at": ["date", "gte", "lte"],
"region": ["exact", "icontains", "in"],
}
class ServiceOverviewFilter(ScanSummaryFilter):
muted_findings = None
def is_valid(self):
# Check if at least one of the inserted_at filters is present
inserted_at_filters = [
self.data.get("inserted_at"),
self.data.get("inserted_at__gte"),
self.data.get("inserted_at__lte"),
]
if not any(inserted_at_filters):
raise ValidationError(
{
"inserted_at": [
"At least one of filter[inserted_at], filter[inserted_at__gte], or "
"filter[inserted_at__lte] is required."
]
}
)
return super().is_valid()

View File

@@ -6,6 +6,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.823Z",
"updated_at": "2024-10-18T10:46:04.841Z",
"first_seen_at": "2024-10-18T10:46:04.823Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-south-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -61,6 +62,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.855Z",
"updated_at": "2024-10-18T10:46:04.858Z",
"first_seen_at": "2024-10-18T10:46:04.855Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-3-112233445566",
"delta": "new",
"status": "FAIL",
@@ -116,6 +118,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.869Z",
"updated_at": "2024-10-18T10:46:04.876Z",
"first_seen_at": "2024-10-18T10:46:04.869Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -171,6 +174,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.888Z",
"updated_at": "2024-10-18T10:46:04.892Z",
"first_seen_at": "2024-10-18T10:46:04.888Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -226,6 +230,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.901Z",
"updated_at": "2024-10-18T10:46:04.905Z",
"first_seen_at": "2024-10-18T10:46:04.901Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -281,6 +286,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.915Z",
"updated_at": "2024-10-18T10:46:04.919Z",
"first_seen_at": "2024-10-18T10:46:04.915Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-south-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -336,6 +342,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.929Z",
"updated_at": "2024-10-18T10:46:04.934Z",
"first_seen_at": "2024-10-18T10:46:04.929Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -391,6 +398,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.944Z",
"updated_at": "2024-10-18T10:46:04.947Z",
"first_seen_at": "2024-10-18T10:46:04.944Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ca-central-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -446,6 +454,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.957Z",
"updated_at": "2024-10-18T10:46:04.962Z",
"first_seen_at": "2024-10-18T10:46:04.957Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-1-ConsoleAnalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c",
"delta": "new",
"status": "PASS",
@@ -501,6 +510,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.971Z",
"updated_at": "2024-10-18T10:46:04.975Z",
"first_seen_at": "2024-10-18T10:46:04.971Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -556,6 +566,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.984Z",
"updated_at": "2024-10-18T10:46:04.989Z",
"first_seen_at": "2024-10-18T10:46:04.984Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-sa-east-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -611,6 +622,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:04.999Z",
"updated_at": "2024-10-18T10:46:05.003Z",
"first_seen_at": "2024-10-18T10:46:04.999Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-north-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -666,6 +678,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.013Z",
"updated_at": "2024-10-18T10:46:05.018Z",
"first_seen_at": "2024-10-18T10:46:05.013Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -721,6 +734,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.029Z",
"updated_at": "2024-10-18T10:46:05.033Z",
"first_seen_at": "2024-10-18T10:46:05.029Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -776,6 +790,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.045Z",
"updated_at": "2024-10-18T10:46:05.050Z",
"first_seen_at": "2024-10-18T10:46:05.045Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -831,6 +846,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.061Z",
"updated_at": "2024-10-18T10:46:05.065Z",
"first_seen_at": "2024-10-18T10:46:05.061Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-1-112233445566",
"delta": "new",
"status": "FAIL",
@@ -886,6 +902,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.080Z",
"updated_at": "2024-10-18T10:46:05.085Z",
"first_seen_at": "2024-10-18T10:46:05.080Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -941,6 +958,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.099Z",
"updated_at": "2024-10-18T10:46:05.104Z",
"first_seen_at": "2024-10-18T10:46:05.099Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-2-112233445566",
"delta": "new",
"status": "FAIL",
@@ -996,6 +1014,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T10:46:05.115Z",
"updated_at": "2024-10-18T10:46:05.121Z",
"first_seen_at": "2024-10-18T10:46:05.115Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-3-112233445566",
"delta": "new",
"status": "FAIL",
@@ -1051,6 +1070,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.489Z",
"updated_at": "2024-10-18T11:16:24.506Z",
"first_seen_at": "2024-10-18T10:46:04.823Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-south-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1106,6 +1126,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.518Z",
"updated_at": "2024-10-18T11:16:24.521Z",
"first_seen_at": "2024-10-18T10:46:04.855Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-3-112233445566",
"delta": null,
"status": "FAIL",
@@ -1161,6 +1182,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.526Z",
"updated_at": "2024-10-18T11:16:24.529Z",
"first_seen_at": "2024-10-18T10:46:04.869Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1216,6 +1238,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.535Z",
"updated_at": "2024-10-18T11:16:24.538Z",
"first_seen_at": "2024-10-18T10:46:04.888Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1271,6 +1294,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.544Z",
"updated_at": "2024-10-18T11:16:24.546Z",
"first_seen_at": "2024-10-18T10:46:04.901Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1326,6 +1350,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.551Z",
"updated_at": "2024-10-18T11:16:24.554Z",
"first_seen_at": "2024-10-18T10:46:04.915Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-south-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1381,6 +1406,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.560Z",
"updated_at": "2024-10-18T11:16:24.562Z",
"first_seen_at": "2024-10-18T10:46:04.929Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1436,6 +1462,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.567Z",
"updated_at": "2024-10-18T11:16:24.569Z",
"first_seen_at": "2024-10-18T10:46:04.944Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ca-central-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1491,6 +1518,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.573Z",
"updated_at": "2024-10-18T11:16:24.575Z",
"first_seen_at": "2024-10-18T10:46:04.957Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-east-1-ConsoleAnalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c",
"delta": null,
"status": "PASS",
@@ -1546,6 +1574,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.580Z",
"updated_at": "2024-10-18T11:16:24.582Z",
"first_seen_at": "2024-10-18T10:46:04.971Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-west-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1601,6 +1630,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.587Z",
"updated_at": "2024-10-18T11:16:24.589Z",
"first_seen_at": "2024-10-18T10:46:04.984Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-sa-east-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1656,6 +1686,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.595Z",
"updated_at": "2024-10-18T11:16:24.597Z",
"first_seen_at": "2024-10-18T10:46:04.999Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-north-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1711,6 +1742,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.602Z",
"updated_at": "2024-10-18T11:16:24.604Z",
"first_seen_at": "2024-10-18T10:46:05.013Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-us-west-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1766,6 +1798,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.610Z",
"updated_at": "2024-10-18T11:16:24.612Z",
"first_seen_at": "2024-10-18T10:46:05.029Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1821,6 +1854,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.617Z",
"updated_at": "2024-10-18T11:16:24.620Z",
"first_seen_at": "2024-10-18T10:46:05.045Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-eu-central-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1876,6 +1910,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.625Z",
"updated_at": "2024-10-18T11:16:24.627Z",
"first_seen_at": "2024-10-18T10:46:05.061Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-1-112233445566",
"delta": null,
"status": "FAIL",
@@ -1931,6 +1966,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.632Z",
"updated_at": "2024-10-18T11:16:24.634Z",
"first_seen_at": "2024-10-18T10:46:05.080Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-southeast-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -1986,6 +2022,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.639Z",
"updated_at": "2024-10-18T11:16:24.642Z",
"first_seen_at": "2024-10-18T10:46:05.099Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-2-112233445566",
"delta": null,
"status": "FAIL",
@@ -2041,6 +2078,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:24.646Z",
"updated_at": "2024-10-18T11:16:24.648Z",
"first_seen_at": "2024-10-18T10:46:05.115Z",
"uid": "prowler-aws-accessanalyzer_enabled-112233445566-ap-northeast-3-112233445566",
"delta": null,
"status": "FAIL",
@@ -2096,6 +2134,7 @@
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"inserted_at": "2024-10-18T11:16:26.033Z",
"updated_at": "2024-10-18T11:16:26.045Z",
"first_seen_at": "2024-10-18T11:16:26.033Z",
"uid": "prowler-aws-account_security_contact_information_is_registered-112233445566-us-east-1-112233445566",
"delta": "new",
"status": "MANUAL",

View File

@@ -58,5 +58,96 @@
"provider_group": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
"inserted_at": "2024-11-13T11:55:41.237Z"
}
},
{
"model": "api.role",
"pk": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "admin_test",
"manage_users": true,
"manage_account": true,
"manage_billing": true,
"manage_providers": true,
"manage_integrations": true,
"manage_scans": true,
"unlimited_visibility": true,
"inserted_at": "2024-11-20T15:32:42.402Z",
"updated_at": "2024-11-20T15:32:42.402Z"
}
},
{
"model": "api.role",
"pk": "845ff03a-87ef-42ba-9786-6577c70c4df0",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "first_role",
"manage_users": true,
"manage_account": true,
"manage_billing": true,
"manage_providers": true,
"manage_integrations": false,
"manage_scans": false,
"unlimited_visibility": true,
"inserted_at": "2024-11-20T15:31:53.239Z",
"updated_at": "2024-11-20T15:31:53.239Z"
}
},
{
"model": "api.role",
"pk": "902d726c-4bd5-413a-a2a4-f7b4754b6b20",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"name": "third_role",
"manage_users": false,
"manage_account": false,
"manage_billing": false,
"manage_providers": false,
"manage_integrations": false,
"manage_scans": true,
"unlimited_visibility": false,
"inserted_at": "2024-11-20T15:34:05.440Z",
"updated_at": "2024-11-20T15:34:05.440Z"
}
},
{
"model": "api.roleprovidergrouprelationship",
"pk": "57fd024a-0a7f-49b4-a092-fa0979a07aaf",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"provider_group": "3fe28fb8-e545-424c-9b8f-69aff638f430",
"inserted_at": "2024-11-20T15:32:42.402Z"
}
},
{
"model": "api.roleprovidergrouprelationship",
"pk": "a3cd0099-1c13-4df1-a5e5-ecdfec561b35",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"provider_group": "481769f5-db2b-447b-8b00-1dee18db90ec",
"inserted_at": "2024-11-20T15:32:42.402Z"
}
},
{
"model": "api.roleprovidergrouprelationship",
"pk": "cfd84182-a058-40c2-af3c-0189b174940f",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"provider_group": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
"inserted_at": "2024-11-20T15:32:42.402Z"
}
},
{
"model": "api.userrolerelationship",
"pk": "92339663-e954-4fd8-98fb-8bfe15949975",
"fields": {
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
"role": "3f01e759-bdf9-4a99-8888-1ab805b79f93",
"user": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
"inserted_at": "2024-11-20T15:36:14.302Z"
}
}
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.1 on 2024-12-20 13:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0002_token_migrations"),
]
operations = [
migrations.RemoveConstraint(
model_name="provider",
name="unique_provider_uids",
),
migrations.AddConstraint(
model_name="provider",
constraint=models.UniqueConstraint(
fields=("tenant_id", "provider", "uid", "is_deleted"),
name="unique_provider_uids",
),
),
]

View File

@@ -0,0 +1,248 @@
# Generated by Django 5.1.1 on 2024-12-05 12:29
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import api.rls
class Migration(migrations.Migration):
dependencies = [
("api", "0003_update_provider_unique_constraint_with_is_deleted"),
]
operations = [
migrations.CreateModel(
name="Role",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(max_length=255)),
("manage_users", models.BooleanField(default=False)),
("manage_account", models.BooleanField(default=False)),
("manage_billing", models.BooleanField(default=False)),
("manage_providers", models.BooleanField(default=False)),
("manage_integrations", models.BooleanField(default=False)),
("manage_scans", models.BooleanField(default=False)),
("unlimited_visibility", models.BooleanField(default=False)),
("inserted_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "roles",
},
),
migrations.CreateModel(
name="RoleProviderGroupRelationship",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "role_provider_group_relationship",
},
),
migrations.CreateModel(
name="UserRoleRelationship",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "role_user_relationship",
},
),
migrations.AddField(
model_name="roleprovidergrouprelationship",
name="provider_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.providergroup"
),
),
migrations.AddField(
model_name="roleprovidergrouprelationship",
name="role",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.role"
),
),
migrations.AddField(
model_name="role",
name="provider_groups",
field=models.ManyToManyField(
related_name="roles",
through="api.RoleProviderGroupRelationship",
to="api.providergroup",
),
),
migrations.AddField(
model_name="userrolerelationship",
name="role",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.role"
),
),
migrations.AddField(
model_name="userrolerelationship",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="role",
name="users",
field=models.ManyToManyField(
related_name="roles",
through="api.UserRoleRelationship",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddConstraint(
model_name="roleprovidergrouprelationship",
constraint=models.UniqueConstraint(
fields=("role_id", "provider_group_id"),
name="unique_role_provider_group_relationship",
),
),
migrations.AddConstraint(
model_name="roleprovidergrouprelationship",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_roleprovidergrouprelationship",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.AddConstraint(
model_name="userrolerelationship",
constraint=models.UniqueConstraint(
fields=("role_id", "user_id"), name="unique_role_user_relationship"
),
),
migrations.AddConstraint(
model_name="userrolerelationship",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_userrolerelationship",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.AddConstraint(
model_name="role",
constraint=models.UniqueConstraint(
fields=("tenant_id", "name"), name="unique_role_per_tenant"
),
),
migrations.AddConstraint(
model_name="role",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_role",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.CreateModel(
name="InvitationRoleRelationship",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("inserted_at", models.DateTimeField(auto_now_add=True)),
(
"invitation",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.invitation"
),
),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.role"
),
),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "role_invitation_relationship",
},
),
migrations.AddConstraint(
model_name="invitationrolerelationship",
constraint=models.UniqueConstraint(
fields=("role_id", "invitation_id"),
name="unique_role_invitation_relationship",
),
),
migrations.AddConstraint(
model_name="invitationrolerelationship",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_invitationrolerelationship",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
migrations.AddField(
model_name="role",
name="invitations",
field=models.ManyToManyField(
related_name="roles",
through="api.InvitationRoleRelationship",
to="api.invitation",
),
),
]

View File

@@ -0,0 +1,44 @@
from django.db import migrations
from api.db_router import MainRouter
def create_admin_role(apps, schema_editor):
Tenant = apps.get_model("api", "Tenant")
Role = apps.get_model("api", "Role")
User = apps.get_model("api", "User")
UserRoleRelationship = apps.get_model("api", "UserRoleRelationship")
for tenant in Tenant.objects.using(MainRouter.admin_db).all():
admin_role, _ = Role.objects.using(MainRouter.admin_db).get_or_create(
name="admin",
tenant=tenant,
defaults={
"manage_users": True,
"manage_account": True,
"manage_billing": True,
"manage_providers": True,
"manage_integrations": True,
"manage_scans": True,
"unlimited_visibility": True,
},
)
users = User.objects.using(MainRouter.admin_db).filter(
membership__tenant=tenant
)
for user in users:
UserRoleRelationship.objects.using(MainRouter.admin_db).get_or_create(
user=user,
role=admin_role,
tenant=tenant,
)
class Migration(migrations.Migration):
dependencies = [
("api", "0004_rbac"),
]
operations = [
migrations.RunPython(create_admin_role),
]

View File

@@ -0,0 +1,15 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0005_rbac_missing_admin_roles"),
]
operations = [
migrations.AddField(
model_name="finding",
name="first_seen_at",
field=models.DateTimeField(editable=False, null=True),
),
]

View File

@@ -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"
),
),
]

View File

@@ -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),
]

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
@@ -69,6 +70,21 @@ class StateChoices(models.TextChoices):
CANCELLED = "cancelled", _("Cancelled")
class PermissionChoices(models.TextChoices):
"""
Represents the different permission states that a role can have.
Attributes:
UNLIMITED: Indicates that the role possesses all permissions.
LIMITED: Indicates that the role has some permissions but not all.
NONE: Indicates that the role does not have any permissions.
"""
UNLIMITED = "unlimited", _("Unlimited permissions")
LIMITED = "limited", _("Limited permissions")
NONE = "none", _("No permissions")
class ActiveProviderManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(self.active_provider_filter())
@@ -256,7 +272,7 @@ class Provider(RowLevelSecurityProtectedModel):
constraints = [
models.UniqueConstraint(
fields=("tenant_id", "provider", "uid"),
fields=("tenant_id", "provider", "uid", "is_deleted"),
name="unique_provider_uids",
),
RowLevelSecurityConstraint(
@@ -298,19 +314,10 @@ class ProviderGroup(RowLevelSecurityProtectedModel):
class ProviderGroupMembership(RowLevelSecurityProtectedModel):
objects = ActiveProviderManager()
all_objects = models.Manager()
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
provider = models.ForeignKey(
Provider,
on_delete=models.CASCADE,
)
provider_group = models.ForeignKey(
ProviderGroup,
on_delete=models.CASCADE,
)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
provider_group = models.ForeignKey(ProviderGroup, on_delete=models.CASCADE)
provider = models.ForeignKey(Provider, on_delete=models.CASCADE)
inserted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "provider_group_memberships"
@@ -327,7 +334,7 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel):
]
class JSONAPIMeta:
resource_name = "provider-group-memberships"
resource_name = "provider_groups-provider"
class Task(RowLevelSecurityProtectedModel):
@@ -404,6 +411,9 @@ 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
)
# TODO: mutelist foreign key
class Meta(RowLevelSecurityProtectedModel.Meta):
@@ -422,6 +432,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:
@@ -509,8 +523,8 @@ class Resource(RowLevelSecurityProtectedModel):
through="ResourceTagMapping",
)
def get_tags(self) -> dict:
return {tag.key: tag.value for tag in self.tags.all()}
def get_tags(self, tenant_id: str) -> dict:
return {tag.key: tag.value for tag in self.tags.filter(tenant_id=tenant_id)}
def clear_tags(self):
self.tags.clear()
@@ -609,6 +623,7 @@ class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
first_seen_at = models.DateTimeField(editable=False, null=True)
uid = models.CharField(max_length=300)
delta = FindingDeltaEnumField(
@@ -851,6 +866,150 @@ class Invitation(RowLevelSecurityProtectedModel):
resource_name = "invitations"
class Role(RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=255)
manage_users = models.BooleanField(default=False)
manage_account = models.BooleanField(default=False)
manage_billing = models.BooleanField(default=False)
manage_providers = models.BooleanField(default=False)
manage_integrations = models.BooleanField(default=False)
manage_scans = models.BooleanField(default=False)
unlimited_visibility = models.BooleanField(default=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
provider_groups = models.ManyToManyField(
ProviderGroup, through="RoleProviderGroupRelationship", related_name="roles"
)
users = models.ManyToManyField(
User, through="UserRoleRelationship", related_name="roles"
)
invitations = models.ManyToManyField(
Invitation, through="InvitationRoleRelationship", related_name="roles"
)
# Filter permission_state
PERMISSION_FIELDS = [
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
]
@property
def permission_state(self):
values = [getattr(self, field) for field in self.PERMISSION_FIELDS]
if all(values):
return PermissionChoices.UNLIMITED
elif not any(values):
return PermissionChoices.NONE
else:
return PermissionChoices.LIMITED
@classmethod
def filter_by_permission_state(cls, queryset, value):
q_all_true = Q(**{field: True for field in cls.PERMISSION_FIELDS})
q_all_false = Q(**{field: False for field in cls.PERMISSION_FIELDS})
if value == PermissionChoices.UNLIMITED:
return queryset.filter(q_all_true)
elif value == PermissionChoices.NONE:
return queryset.filter(q_all_false)
else:
return queryset.exclude(q_all_true | q_all_false)
class Meta:
db_table = "roles"
constraints = [
models.UniqueConstraint(
fields=["tenant_id", "name"],
name="unique_role_per_tenant",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class JSONAPIMeta:
resource_name = "roles"
class RoleProviderGroupRelationship(RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
provider_group = models.ForeignKey(ProviderGroup, on_delete=models.CASCADE)
inserted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "role_provider_group_relationship"
constraints = [
models.UniqueConstraint(
fields=["role_id", "provider_group_id"],
name="unique_role_provider_group_relationship",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class JSONAPIMeta:
resource_name = "role-provider_groups"
class UserRoleRelationship(RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
inserted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "role_user_relationship"
constraints = [
models.UniqueConstraint(
fields=["role_id", "user_id"],
name="unique_role_user_relationship",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class JSONAPIMeta:
resource_name = "user-roles"
class InvitationRoleRelationship(RowLevelSecurityProtectedModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
invitation = models.ForeignKey(Invitation, on_delete=models.CASCADE)
inserted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "role_invitation_relationship"
constraints = [
models.UniqueConstraint(
fields=["role_id", "invitation_id"],
name="unique_role_invitation_relationship",
),
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class JSONAPIMeta:
resource_name = "invitation-roles"
class ComplianceOverview(RowLevelSecurityProtectedModel):
objects = ActiveProviderManager()
all_objects = models.Manager()
@@ -949,6 +1108,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"

View File

@@ -0,0 +1,75 @@
from enum import Enum
from typing import Optional
from django.db.models import QuerySet
from rest_framework.permissions import BasePermission
from api.db_router import MainRouter
from api.models import Provider, Role, User
class Permissions(Enum):
MANAGE_USERS = "manage_users"
MANAGE_ACCOUNT = "manage_account"
MANAGE_BILLING = "manage_billing"
MANAGE_PROVIDERS = "manage_providers"
MANAGE_INTEGRATIONS = "manage_integrations"
MANAGE_SCANS = "manage_scans"
UNLIMITED_VISIBILITY = "unlimited_visibility"
class HasPermissions(BasePermission):
"""
Custom permission to check if the user's role has the required permissions.
The required permissions should be specified in the view as a list in `required_permissions`.
"""
def has_permission(self, request, view):
required_permissions = getattr(view, "required_permissions", [])
if not required_permissions:
return True
user_roles = (
User.objects.using(MainRouter.admin_db).get(id=request.user.id).roles.all()
)
if not user_roles:
return False
for perm in required_permissions:
if not getattr(user_roles[0], perm.value, False):
return False
return True
def get_role(user: User) -> Optional[Role]:
"""
Retrieve the first role assigned to the given user.
Returns:
The user's first Role instance if the user has any roles, otherwise None.
"""
return user.roles.first()
def get_providers(role: Role) -> QuerySet[Provider]:
"""
Return a distinct queryset of Providers accessible by the given role.
If the role has no associated provider groups, an empty queryset is returned.
Args:
role: A Role instance.
Returns:
A QuerySet of Provider objects filtered by the role's provider groups.
If the role has no provider groups, returns an empty queryset.
"""
tenant = role.tenant
provider_groups = role.provider_groups.all()
if not provider_groups.exists():
return Provider.objects.none()
return Provider.objects.filter(
tenant=tenant, provider_groups__in=provider_groups
).distinct()

View File

@@ -2,7 +2,7 @@ from contextlib import nullcontext
from rest_framework_json_api.renderers import JSONRenderer
from api.db_utils import tenant_transaction
from api.db_utils import rls_transaction
class APIJSONRenderer(JSONRenderer):
@@ -13,9 +13,9 @@ class APIJSONRenderer(JSONRenderer):
tenant_id = getattr(request, "tenant_id", None) if request else None
include_param_present = "include" in request.query_params if request else False
# Use tenant_transaction if needed for included resources, otherwise do nothing
# Use rls_transaction if needed for included resources, otherwise do nothing
context_manager = (
tenant_transaction(tenant_id)
rls_transaction(tenant_id)
if tenant_id and include_param_present
else nullcontext()
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,9 @@
import pytest
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
from django.urls import reverse
from unittest.mock import patch
from rest_framework.test import APIClient
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
@patch("api.v1.views.MainRouter.admin_db", new="default")
@pytest.mark.django_db
def test_basic_authentication():
client = APIClient()
@@ -98,3 +95,85 @@ def test_refresh_token(create_test_user, tenants_fixture):
format="vnd.api+json",
)
assert new_refresh_response.status_code == 200
@pytest.mark.django_db
def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fixture):
client = APIClient()
role = roles_fixture[0]
user1_email = "user1@testing.com"
user2_email = "user2@testing.com"
password = "thisisapassword123"
user1_response = client.post(
reverse("user-list"),
data={
"data": {
"type": "users",
"attributes": {
"name": "user1",
"email": user1_email,
"password": password,
},
}
},
format="vnd.api+json",
)
assert user1_response.status_code == 201
user1_access_token, _ = get_api_tokens(client, user1_email, password)
user1_headers = get_authorization_header(user1_access_token)
user2_invitation = client.post(
reverse("invitation-list"),
data={
"data": {
"type": "invitations",
"attributes": {"email": user2_email},
"relationships": {
"roles": {
"data": [
{
"type": "roles",
"id": str(role.id),
}
]
}
},
}
},
format="vnd.api+json",
headers=user1_headers,
)
assert user2_invitation.status_code == 201
invitation_token = user2_invitation.json()["data"]["attributes"]["token"]
user2_response = client.post(
reverse("user-list") + f"?invitation_token={invitation_token}",
data={
"data": {
"type": "users",
"attributes": {
"name": "user2",
"email": user2_email,
"password": password,
},
}
},
format="vnd.api+json",
)
assert user2_response.status_code == 201
user2_access_token, _ = get_api_tokens(client, user2_email, password)
user2_headers = get_authorization_header(user2_access_token)
user1_me = client.get(reverse("user-me"), headers=user1_headers)
assert user1_me.status_code == 200
assert user1_me.json()["data"]["attributes"]["email"] == user1_email
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

View File

@@ -0,0 +1,85 @@
from unittest.mock import Mock, patch
import pytest
from conftest import get_api_tokens, get_authorization_header
from django.urls import reverse
from rest_framework.test import APIClient
from api.models import Provider
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.delete_provider_task.delay")
@pytest.mark.django_db
def test_delete_provider_without_executing_task(
mock_delete_task, mock_task_get, create_test_user, tenants_fixture, tasks_fixture
):
client = APIClient()
test_user = "test_email@prowler.com"
test_password = "test_password"
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
mock_delete_task.return_value = task_mock
mock_task_get.return_value = prowler_task
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
access_token, _ = get_api_tokens(client, test_user, test_password)
auth_headers = get_authorization_header(access_token)
create_provider_response = client.post(
reverse("provider-list"),
data={
"data": {
"type": "providers",
"attributes": {
"provider": Provider.ProviderChoices.AWS,
"uid": "123456789012",
},
}
},
format="vnd.api+json",
headers=auth_headers,
)
assert create_provider_response.status_code == 201
provider_id = create_provider_response.json()["data"]["id"]
provider_uid = create_provider_response.json()["data"]["attributes"]["uid"]
remove_provider = client.delete(
reverse("provider-detail", kwargs={"pk": provider_id}),
headers=auth_headers,
)
assert remove_provider.status_code == 202
recreate_provider_response = client.post(
reverse("provider-list"),
data={
"data": {
"type": "providers",
"attributes": {
"provider": Provider.ProviderChoices.AWS,
"uid": provider_uid,
},
}
},
format="vnd.api+json",
headers=auth_headers,
)
assert recreate_provider_response.status_code == 201

View File

@@ -13,6 +13,7 @@ def test_check_resources_between_different_tenants(
enforce_test_user_db_connection,
authenticated_api_client,
tenants_fixture,
set_user_admin_roles_fixture,
):
client = authenticated_api_client

View File

@@ -6,8 +6,10 @@ from django.db.utils import ConnectionRouter
from api.db_router import MainRouter
from api.rls import Tenant
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
from unittest.mock import patch
@patch("api.db_router.MainRouter.admin_db", new="admin")
class TestMainDatabaseRouter:
@pytest.fixture(scope="module")
def router(self):

View File

@@ -2,7 +2,15 @@ from datetime import datetime, timezone
from enum import Enum
from unittest.mock import patch
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
import pytest
from api.db_utils import (
batch_delete,
enum_to_choices,
generate_random_token,
one_week_from_now,
)
from api.models import Provider
class TestEnumToChoices:
@@ -106,3 +114,26 @@ class TestGenerateRandomToken:
token = generate_random_token(length=5, symbols="")
# Default symbols
assert len(token) == 5
class TestBatchDelete:
@pytest.fixture
def create_test_providers(self, tenants_fixture):
tenant = tenants_fixture[0]
provider_id = 123456789012
provider_count = 10
for i in range(provider_count):
Provider.objects.create(
tenant=tenant,
uid=f"{provider_id + i}",
provider=Provider.ProviderChoices.AWS,
)
return provider_count
@pytest.mark.django_db
def test_batch_delete(self, create_test_providers):
_, summary = batch_delete(
Provider.objects.all(), batch_size=create_test_providers // 2
)
assert Provider.objects.all().count() == 0
assert summary == {"api.Provider": create_test_providers}

View File

@@ -1,7 +1,9 @@
from unittest.mock import patch, call
import uuid
from unittest.mock import call, patch
import pytest
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
from api.decorators import set_tenant
@@ -15,12 +17,12 @@ class TestSetTenantDecorator:
def random_func(arg):
return arg
tenant_id = "1234-abcd-5678"
tenant_id = str(uuid.uuid4())
result = random_func("test_arg", tenant_id=tenant_id)
assert (
call(f"SELECT set_config('api.tenant_id', '{tenant_id}', TRUE);")
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
in mock_cursor.execute.mock_calls
)
assert result == "test_arg"

View File

@@ -7,9 +7,10 @@ from api.models import Resource, ResourceTag
class TestResourceModel:
def test_setting_tags(self, providers_fixture):
provider, *_ = providers_fixture
tenant_id = provider.tenant_id
resource = Resource.objects.create(
tenant_id=provider.tenant_id,
tenant_id=tenant_id,
provider=provider,
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
name="My Instance 1",
@@ -20,12 +21,12 @@ class TestResourceModel:
tags = [
ResourceTag.objects.create(
tenant_id=provider.tenant_id,
tenant_id=tenant_id,
key="key",
value="value",
),
ResourceTag.objects.create(
tenant_id=provider.tenant_id,
tenant_id=tenant_id,
key="key2",
value="value2",
),
@@ -33,9 +34,9 @@ class TestResourceModel:
resource.upsert_or_delete_tags(tags)
assert len(tags) == len(resource.tags.all())
assert len(tags) == len(resource.tags.filter(tenant_id=tenant_id))
tags_dict = resource.get_tags()
tags_dict = resource.get_tags(tenant_id=tenant_id)
for tag in tags:
assert tag.key in tags_dict
@@ -43,47 +44,51 @@ class TestResourceModel:
def test_adding_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
tags = [
ResourceTag.objects.create(
tenant_id=resource.tenant_id,
tenant_id=tenant_id,
key="env",
value="test",
),
]
before_count = len(resource.tags.all())
before_count = len(resource.tags.filter(tenant_id=tenant_id))
resource.upsert_or_delete_tags(tags)
assert before_count + 1 == len(resource.tags.all())
assert before_count + 1 == len(resource.tags.filter(tenant_id=tenant_id))
tags_dict = resource.get_tags()
tags_dict = resource.get_tags(tenant_id=tenant_id)
assert "env" in tags_dict
assert tags_dict["env"] == "test"
def test_adding_duplicate_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
tags = resource.tags.all()
tags = resource.tags.filter(tenant_id=tenant_id)
before_count = len(resource.tags.all())
before_count = len(resource.tags.filter(tenant_id=tenant_id))
resource.upsert_or_delete_tags(tags)
# should be the same number of tags
assert before_count == len(resource.tags.all())
assert before_count == len(resource.tags.filter(tenant_id=tenant_id))
def test_add_tags_none(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
resource.upsert_or_delete_tags(None)
assert len(resource.tags.all()) == 0
assert resource.get_tags() == {}
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
assert resource.get_tags(tenant_id=tenant_id) == {}
def test_clear_tags(self, resources_fixture):
resource, *_ = resources_fixture
tenant_id = str(resource.tenant_id)
resource.clear_tags()
assert len(resource.tags.all()) == 0
assert resource.get_tags() == {}
assert len(resource.tags.filter(tenant_id=tenant_id)) == 0
assert resource.get_tags(tenant_id=tenant_id) == {}

View File

@@ -0,0 +1,306 @@
import pytest
from django.urls import reverse
from rest_framework import status
from unittest.mock import patch, ANY, Mock
@pytest.mark.django_db
class TestUserViewSet:
def test_list_users_with_all_permissions(self, authenticated_client_rbac):
response = authenticated_client_rbac.get(reverse("user-list"))
assert response.status_code == status.HTTP_200_OK
assert isinstance(response.json()["data"], list)
def test_list_users_with_no_permissions(
self, authenticated_client_no_permissions_rbac
):
response = authenticated_client_no_permissions_rbac.get(reverse("user-list"))
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_retrieve_user_with_all_permissions(
self, authenticated_client_rbac, create_test_user_rbac
):
response = authenticated_client_rbac.get(
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id})
)
assert response.status_code == status.HTTP_200_OK
assert (
response.json()["data"]["attributes"]["email"]
== create_test_user_rbac.email
)
def test_retrieve_user_with_no_roles(
self, authenticated_client_rbac_noroles, create_test_user_rbac_no_roles
):
response = authenticated_client_rbac_noroles.get(
reverse("user-detail", kwargs={"pk": create_test_user_rbac_no_roles.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_retrieve_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac, create_test_user
):
response = authenticated_client_no_permissions_rbac.get(
reverse("user-detail", kwargs={"pk": create_test_user.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_user_with_all_permissions(self, authenticated_client_rbac):
valid_user_payload = {
"name": "test",
"password": "newpassword123",
"email": "new_user@test.com",
}
response = authenticated_client_rbac.post(
reverse("user-list"), data=valid_user_payload, format="vnd.api+json"
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["data"]["attributes"]["email"] == "new_user@test.com"
def test_create_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac
):
valid_user_payload = {
"name": "test",
"password": "newpassword123",
"email": "new_user@test.com",
}
response = authenticated_client_no_permissions_rbac.post(
reverse("user-list"), data=valid_user_payload, format="vnd.api+json"
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["data"]["attributes"]["email"] == "new_user@test.com"
def test_partial_update_user_with_all_permissions(
self, authenticated_client_rbac, create_test_user_rbac
):
updated_data = {
"data": {
"type": "users",
"id": str(create_test_user_rbac.id),
"attributes": {"name": "Updated Name"},
},
}
response = authenticated_client_rbac.patch(
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id}),
data=updated_data,
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["name"] == "Updated Name"
def test_partial_update_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac, create_test_user
):
updated_data = {
"data": {
"type": "users",
"attributes": {"name": "Updated Name"},
}
}
response = authenticated_client_no_permissions_rbac.patch(
reverse("user-detail", kwargs={"pk": create_test_user.id}),
data=updated_data,
format="vnd.api+json",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_delete_user_with_all_permissions(
self, authenticated_client_rbac, create_test_user_rbac
):
response = authenticated_client_rbac.delete(
reverse("user-detail", kwargs={"pk": create_test_user_rbac.id})
)
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_delete_user_with_no_permissions(
self, authenticated_client_no_permissions_rbac, create_test_user
):
response = authenticated_client_no_permissions_rbac.delete(
reverse("user-detail", kwargs={"pk": create_test_user.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_me_with_all_permissions(
self, authenticated_client_rbac, create_test_user_rbac
):
response = authenticated_client_rbac.get(reverse("user-me"))
assert response.status_code == status.HTTP_200_OK
assert (
response.json()["data"]["attributes"]["email"]
== create_test_user_rbac.email
)
def test_me_with_no_permissions(
self, authenticated_client_no_permissions_rbac, create_test_user
):
response = authenticated_client_no_permissions_rbac.get(reverse("user-me"))
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["email"] == "rbac_limited@rbac.com"
@pytest.mark.django_db
class TestProviderViewSet:
def test_list_providers_with_all_permissions(
self, authenticated_client_rbac, providers_fixture
):
response = authenticated_client_rbac.get(reverse("provider-list"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(providers_fixture)
def test_list_providers_with_no_permissions(
self, authenticated_client_no_permissions_rbac
):
response = authenticated_client_no_permissions_rbac.get(
reverse("provider-list")
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 0
def test_retrieve_provider_with_all_permissions(
self, authenticated_client_rbac, providers_fixture
):
provider = providers_fixture[0]
response = authenticated_client_rbac.get(
reverse("provider-detail", kwargs={"pk": provider.id})
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["alias"] == provider.alias
def test_retrieve_provider_with_no_permissions(
self, authenticated_client_no_permissions_rbac, providers_fixture
):
provider = providers_fixture[0]
response = authenticated_client_no_permissions_rbac.get(
reverse("provider-detail", kwargs={"pk": provider.id})
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_create_provider_with_all_permissions(self, authenticated_client_rbac):
payload = {"provider": "aws", "uid": "111111111111", "alias": "new_alias"}
response = authenticated_client_rbac.post(
reverse("provider-list"), data=payload, format="json"
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["data"]["attributes"]["alias"] == "new_alias"
def test_create_provider_with_no_permissions(
self, authenticated_client_no_permissions_rbac
):
payload = {"provider": "aws", "uid": "111111111111", "alias": "new_alias"}
response = authenticated_client_no_permissions_rbac.post(
reverse("provider-list"), data=payload, format="json"
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_partial_update_provider_with_all_permissions(
self, authenticated_client_rbac, providers_fixture
):
provider = providers_fixture[0]
payload = {
"data": {
"type": "providers",
"id": provider.id,
"attributes": {"alias": "updated_alias"},
},
}
response = authenticated_client_rbac.patch(
reverse("provider-detail", kwargs={"pk": provider.id}),
data=payload,
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["alias"] == "updated_alias"
def test_partial_update_provider_with_no_permissions(
self, authenticated_client_no_permissions_rbac, providers_fixture
):
provider = providers_fixture[0]
update_payload = {
"data": {
"type": "providers",
"attributes": {"alias": "updated_alias"},
}
}
response = authenticated_client_no_permissions_rbac.patch(
reverse("provider-detail", kwargs={"pk": provider.id}),
data=update_payload,
format="vnd.api+json",
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.delete_provider_task.delay")
def test_delete_provider_with_all_permissions(
self,
mock_delete_task,
mock_task_get,
authenticated_client_rbac,
providers_fixture,
tasks_fixture,
):
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
mock_delete_task.return_value = task_mock
mock_task_get.return_value = prowler_task
provider1, *_ = providers_fixture
response = authenticated_client_rbac.delete(
reverse("provider-detail", kwargs={"pk": provider1.id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
mock_delete_task.assert_called_once_with(
provider_id=str(provider1.id), tenant_id=ANY
)
assert "Content-Location" in response.headers
assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}"
def test_delete_provider_with_no_permissions(
self, authenticated_client_no_permissions_rbac, providers_fixture
):
provider = providers_fixture[0]
response = authenticated_client_no_permissions_rbac.delete(
reverse("provider-detail", kwargs={"pk": provider.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@patch("api.v1.views.Task.objects.get")
@patch("api.v1.views.check_provider_connection_task.delay")
def test_connection_with_all_permissions(
self,
mock_provider_connection,
mock_task_get,
authenticated_client_rbac,
providers_fixture,
tasks_fixture,
):
prowler_task = tasks_fixture[0]
task_mock = Mock()
task_mock.id = prowler_task.id
task_mock.status = "PENDING"
mock_provider_connection.return_value = task_mock
mock_task_get.return_value = prowler_task
provider1, *_ = providers_fixture
assert provider1.connected is None
assert provider1.connection_last_checked_at is None
response = authenticated_client_rbac.post(
reverse("provider-connection", kwargs={"pk": provider1.id})
)
assert response.status_code == status.HTTP_202_ACCEPTED
mock_provider_connection.assert_called_once_with(
provider_id=str(provider1.id), tenant_id=ANY
)
assert "Content-Location" in response.headers
assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}"
def test_connection_with_no_permissions(
self, authenticated_client_no_permissions_rbac, providers_fixture
):
provider = providers_fixture[0]
response = authenticated_client_no_permissions_rbac.post(
reverse("provider-connection", kwargs={"pk": provider.id})
)
assert response.status_code == status.HTTP_403_FORBIDDEN

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ from api.models import (
ComplianceOverview,
Finding,
Invitation,
InvitationRoleRelationship,
Membership,
Provider,
ProviderGroup,
@@ -24,10 +25,13 @@ from api.models import (
ProviderSecret,
Resource,
ResourceTag,
Role,
RoleProviderGroupRelationship,
Scan,
StateChoices,
Task,
User,
UserRoleRelationship,
)
from api.rls import Tenant
@@ -176,10 +180,26 @@ class UserSerializer(BaseSerializerV1):
"""
memberships = serializers.ResourceRelatedField(many=True, read_only=True)
roles = serializers.ResourceRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ["id", "name", "email", "company_name", "date_joined", "memberships"]
fields = [
"id",
"name",
"email",
"company_name",
"date_joined",
"memberships",
"roles",
]
extra_kwargs = {
"roles": {"read_only": True},
}
included_serializers = {
"roles": "api.v1.serializers.RoleSerializer",
}
class UserCreateSerializer(BaseWriteSerializer):
@@ -235,6 +255,73 @@ class UserUpdateSerializer(BaseWriteSerializer):
return super().update(instance, validated_data)
class RoleResourceIdentifierSerializer(serializers.Serializer):
resource_type = serializers.CharField(source="type")
id = serializers.UUIDField()
class JSONAPIMeta:
resource_name = "role-identifier"
def to_representation(self, instance):
"""
Ensure 'type' is used in the output instead of 'resource_type'.
"""
representation = super().to_representation(instance)
representation["type"] = representation.pop("resource_type", None)
return representation
def to_internal_value(self, data):
"""
Map 'type' back to 'resource_type' during input.
"""
data["resource_type"] = data.pop("type", None)
return super().to_internal_value(data)
class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for modifying user memberships
"""
roles = serializers.ListField(
child=RoleResourceIdentifierSerializer(),
help_text="List of resource identifier objects representing roles.",
)
def create(self, validated_data):
role_ids = [item["id"] for item in validated_data["roles"]]
roles = Role.objects.filter(id__in=role_ids)
tenant_id = self.context.get("tenant_id")
new_relationships = [
UserRoleRelationship(
user=self.context.get("user"), role=r, tenant_id=tenant_id
)
for r in roles
]
UserRoleRelationship.objects.bulk_create(new_relationships)
return self.context.get("user")
def update(self, instance, validated_data):
role_ids = [item["id"] for item in validated_data["roles"]]
roles = Role.objects.filter(id__in=role_ids)
tenant_id = self.context.get("tenant_id")
instance.roles.clear()
new_relationships = [
UserRoleRelationship(user=instance, role=r, tenant_id=tenant_id)
for r in roles
]
UserRoleRelationship.objects.bulk_create(new_relationships)
return instance
class Meta:
model = UserRoleRelationship
fields = ["id", "roles"]
# Tasks
class TaskBase(serializers.ModelSerializer):
state_mapping = {
@@ -358,89 +445,199 @@ class MembershipSerializer(serializers.ModelSerializer):
# Provider Groups
class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
providers = serializers.ResourceRelatedField(many=True, read_only=True)
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
def validate(self, attrs):
tenant = self.context["tenant_id"]
name = attrs.get("name", self.instance.name if self.instance else None)
# Exclude the current instance when checking for uniqueness during updates
queryset = ProviderGroup.objects.filter(tenant=tenant, name=name)
if self.instance:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
if ProviderGroup.objects.filter(name=attrs.get("name")).exists():
raise serializers.ValidationError(
{
"name": "A provider group with this name already exists for this tenant."
}
{"name": "A provider group with this name already exists."}
)
return super().validate(attrs)
class Meta:
model = ProviderGroup
fields = ["id", "name", "inserted_at", "updated_at", "providers", "url"]
read_only_fields = ["id", "inserted_at", "updated_at"]
fields = [
"id",
"name",
"inserted_at",
"updated_at",
"providers",
"roles",
"url",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"roles": {"read_only": True},
"url": {"read_only": True},
}
class ProviderGroupIncludedSerializer(RLSSerializer, BaseWriteSerializer):
class ProviderGroupIncludedSerializer(ProviderGroupSerializer):
class Meta:
model = ProviderGroup
fields = ["id", "name"]
class ProviderGroupUpdateSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for updating the ProviderGroup model.
Only allows "name" field to be updated.
"""
class Meta:
model = ProviderGroup
fields = ["id", "name"]
class ProviderGroupMembershipUpdateSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for modifying provider group memberships
"""
provider_ids = serializers.ListField(
child=serializers.UUIDField(),
help_text="List of provider UUIDs to add to the group",
class ProviderGroupCreateSerializer(ProviderGroupSerializer):
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
def validate(self, attrs):
tenant_id = self.context["tenant_id"]
provider_ids = attrs.get("provider_ids", [])
class Meta:
model = ProviderGroup
fields = [
"id",
"name",
"inserted_at",
"updated_at",
"providers",
"roles",
]
existing_provider_ids = set(
Provider.objects.filter(
id__in=provider_ids, tenant_id=tenant_id
).values_list("id", flat=True)
def create(self, validated_data):
providers = validated_data.pop("providers", [])
roles = validated_data.pop("roles", [])
tenant_id = self.context.get("tenant_id")
provider_group = ProviderGroup.objects.create(
tenant_id=tenant_id, **validated_data
)
provided_provider_ids = set(provider_ids)
missing_provider_ids = provided_provider_ids - existing_provider_ids
if missing_provider_ids:
raise serializers.ValidationError(
{
"provider_ids": f"The following provider IDs do not exist: {', '.join(str(id) for id in missing_provider_ids)}"
}
through_model_instances = [
ProviderGroupMembership(
provider_group=provider_group,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
return super().validate(attrs)
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=provider_group,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return provider_group
class ProviderGroupUpdateSerializer(ProviderGroupSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "providers" in validated_data:
providers = validated_data.pop("providers")
instance.providers.clear()
through_model_instances = [
ProviderGroupMembership(
provider_group=instance,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=instance,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
class ProviderResourceIdentifierSerializer(serializers.Serializer):
resource_type = serializers.CharField(source="type")
id = serializers.UUIDField()
class JSONAPIMeta:
resource_name = "provider-identifier"
def to_representation(self, instance):
"""
Ensure 'type' is used in the output instead of 'resource_type'.
"""
representation = super().to_representation(instance)
representation["type"] = representation.pop("resource_type", None)
return representation
def to_internal_value(self, data):
"""
Map 'type' back to 'resource_type' during input.
"""
data["resource_type"] = data.pop("type", None)
return super().to_internal_value(data)
class ProviderGroupMembershipSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for modifying provider_group memberships
"""
providers = serializers.ListField(
child=ProviderResourceIdentifierSerializer(),
help_text="List of resource identifier objects representing providers.",
)
def create(self, validated_data):
provider_ids = [item["id"] for item in validated_data["providers"]]
providers = Provider.objects.filter(id__in=provider_ids)
tenant_id = self.context.get("tenant_id")
new_relationships = [
ProviderGroupMembership(
provider_group=self.context.get("provider_group"),
provider=p,
tenant_id=tenant_id,
)
for p in providers
]
ProviderGroupMembership.objects.bulk_create(new_relationships)
return self.context.get("provider_group")
def update(self, instance, validated_data):
provider_ids = [item["id"] for item in validated_data["providers"]]
providers = Provider.objects.filter(id__in=provider_ids)
tenant_id = self.context.get("tenant_id")
instance.providers.clear()
new_relationships = [
ProviderGroupMembership(
provider_group=instance, provider=p, tenant_id=tenant_id
)
for p in providers
]
ProviderGroupMembership.objects.bulk_create(new_relationships)
return instance
class Meta:
model = ProviderGroupMembership
fields = ["id", "provider_ids"]
fields = ["id", "providers"]
# Providers
@@ -622,6 +819,14 @@ class ScanTaskSerializer(RLSSerializer):
]
class ScanReportSerializer(RLSSerializer):
class Meta:
model = Scan
fields = [
"id",
]
class ResourceTagSerializer(RLSSerializer):
"""
Serializer for the ResourceTag model
@@ -677,7 +882,7 @@ class ResourceSerializer(RLSSerializer):
}
)
def get_tags(self, obj):
return obj.get_tags()
return obj.get_tags(self.context.get("tenant_id"))
def get_fields(self):
"""`type` is a Python reserved keyword."""
@@ -708,6 +913,7 @@ class FindingSerializer(RLSSerializer):
"raw_result",
"inserted_at",
"updated_at",
"first_seen_at",
"url",
# Relationships
"scan",
@@ -720,6 +926,7 @@ class FindingSerializer(RLSSerializer):
}
# To be removed when the related endpoint is removed as well
class FindingDynamicFilterSerializer(serializers.Serializer):
services = serializers.ListField(child=serializers.CharField(), allow_empty=True)
regions = serializers.ListField(child=serializers.CharField(), allow_empty=True)
@@ -728,6 +935,19 @@ class FindingDynamicFilterSerializer(serializers.Serializer):
resource_name = "finding-dynamic-filters"
class FindingMetadataSerializer(serializers.Serializer):
services = serializers.ListField(child=serializers.CharField(), allow_empty=True)
regions = serializers.ListField(child=serializers.CharField(), allow_empty=True)
resource_types = serializers.ListField(
child=serializers.CharField(), allow_empty=True
)
# 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"
# Provider secrets
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
@staticmethod
@@ -800,7 +1020,7 @@ class KubernetesProviderSecret(serializers.Serializer):
class AWSRoleAssumptionProviderSecret(serializers.Serializer):
role_arn = serializers.CharField()
external_id = serializers.CharField(required=False)
external_id = serializers.CharField()
role_session_name = serializers.CharField(required=False)
session_duration = serializers.IntegerField(
required=False, min_value=900, max_value=43200
@@ -847,6 +1067,10 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
"description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role "
"assumption.",
},
"external_id": {
"type": "string",
"description": "An identifier to enhance security for role assumption.",
},
"aws_access_key_id": {
"type": "string",
"description": "The AWS access key ID. Only required if the environment lacks pre-configured "
@@ -868,11 +1092,6 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
"default": 3600,
"description": "The duration (in seconds) for the role session.",
},
"external_id": {
"type": "string",
"description": "An optional identifier to enhance security for role assumption; may be "
"required by the role administrator.",
},
"role_session_name": {
"type": "string",
"description": "An identifier for the role session, useful for tracking sessions in AWS logs. "
@@ -886,7 +1105,7 @@ class AWSRoleAssumptionProviderSecret(serializers.Serializer):
"pattern": "^[a-zA-Z0-9=,.@_-]+$",
},
},
"required": ["role_arn"],
"required": ["role_arn", "external_id"],
},
{
"type": "object",
@@ -1034,6 +1253,14 @@ class InvitationSerializer(RLSSerializer):
Serializer for the Invitation model.
"""
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
class Meta:
model = Invitation
fields = [
@@ -1043,6 +1270,7 @@ class InvitationSerializer(RLSSerializer):
"email",
"state",
"token",
"roles",
"expires_at",
"inviter",
"url",
@@ -1050,6 +1278,14 @@ class InvitationSerializer(RLSSerializer):
class InvitationBaseWriteSerializer(BaseWriteSerializer):
roles = serializers.ResourceRelatedField(many=True, queryset=Role.objects.all())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["roles"].queryset = Role.objects.filter(tenant_id=tenant_id)
def validate_email(self, value):
user = User.objects.filter(email=value).first()
tenant_id = self.context["tenant_id"]
@@ -1086,31 +1322,62 @@ class InvitationCreateSerializer(InvitationBaseWriteSerializer, RLSSerializer):
class Meta:
model = Invitation
fields = ["email", "expires_at", "state", "token", "inviter"]
fields = ["email", "expires_at", "state", "token", "inviter", "roles"]
extra_kwargs = {
"token": {"read_only": True},
"state": {"read_only": True},
"inviter": {"read_only": True},
"expires_at": {"required": False},
"roles": {"required": False},
}
def create(self, validated_data):
inviter = self.context.get("request").user
tenant_id = self.context.get("tenant_id")
validated_data["inviter"] = inviter
return super().create(validated_data)
roles = validated_data.pop("roles", [])
invitation = super().create(validated_data)
for role in roles:
InvitationRoleRelationship.objects.create(
role=role, invitation=invitation, tenant_id=tenant_id
)
return invitation
class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
roles = serializers.ResourceRelatedField(
required=False, many=True, queryset=Role.objects.all()
)
class Meta:
model = Invitation
fields = ["id", "email", "expires_at", "state", "token"]
fields = ["id", "email", "expires_at", "state", "token", "roles"]
extra_kwargs = {
"token": {"read_only": True},
"state": {"read_only": True},
"expires_at": {"required": False},
"email": {"required": False},
"roles": {"required": False},
}
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
new_relationships = [
InvitationRoleRelationship(
role=r, invitation=instance, tenant_id=tenant_id
)
for r in roles
]
InvitationRoleRelationship.objects.bulk_create(new_relationships)
invitation = super().update(instance, validated_data)
return invitation
class InvitationAcceptSerializer(RLSSerializer):
"""Serializer for accepting an invitation."""
@@ -1122,6 +1389,218 @@ class InvitationAcceptSerializer(RLSSerializer):
fields = ["invitation_token"]
# Roles
class RoleSerializer(RLSSerializer, BaseWriteSerializer):
permission_state = serializers.SerializerMethodField()
users = serializers.ResourceRelatedField(
queryset=User.objects.all(), many=True, required=False
)
provider_groups = serializers.ResourceRelatedField(
queryset=ProviderGroup.objects.all(), many=True, required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
tenant_id = self.context.get("tenant_id")
if tenant_id is not None:
self.fields["users"].queryset = User.objects.filter(
membership__tenant__id=tenant_id
)
self.fields["provider_groups"].queryset = ProviderGroup.objects.filter(
tenant_id=self.context.get("tenant_id")
)
def get_permission_state(self, obj) -> str:
return obj.permission_state
def validate(self, attrs):
if Role.objects.filter(name=attrs.get("name")).exists():
raise serializers.ValidationError(
{"name": "A role with this name already exists."}
)
if attrs.get("manage_providers"):
attrs["unlimited_visibility"] = True
# Prevent updates to the admin role
if getattr(self.instance, "name", None) == "admin":
raise serializers.ValidationError(
{"name": "The admin role cannot be updated."}
)
return super().validate(attrs)
class Meta:
model = Role
fields = [
"id",
"name",
"manage_users",
"manage_account",
# Disable for the first release
# "manage_billing",
# "manage_integrations",
# /Disable for the first release
"manage_providers",
"manage_scans",
"permission_state",
"unlimited_visibility",
"inserted_at",
"updated_at",
"provider_groups",
"users",
"invitations",
"url",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"url": {"read_only": True},
}
class RoleCreateSerializer(RoleSerializer):
provider_groups = serializers.ResourceRelatedField(
many=True, queryset=ProviderGroup.objects.all(), required=False
)
users = serializers.ResourceRelatedField(
many=True, queryset=User.objects.all(), required=False
)
def create(self, validated_data):
provider_groups = validated_data.pop("provider_groups", [])
users = validated_data.pop("users", [])
tenant_id = self.context.get("tenant_id")
role = Role.objects.create(tenant_id=tenant_id, **validated_data)
through_model_instances = [
RoleProviderGroupRelationship(
role=role,
provider_group=provider_group,
tenant_id=tenant_id,
)
for provider_group in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
through_model_instances = [
UserRoleRelationship(
role=role,
user=user,
tenant_id=tenant_id,
)
for user in users
]
UserRoleRelationship.objects.bulk_create(through_model_instances)
return role
class RoleUpdateSerializer(RoleSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "provider_groups" in validated_data:
provider_groups = validated_data.pop("provider_groups")
instance.provider_groups.clear()
through_model_instances = [
RoleProviderGroupRelationship(
role=instance,
provider_group=provider_group,
tenant_id=tenant_id,
)
for provider_group in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
if "users" in validated_data:
users = validated_data.pop("users")
instance.users.clear()
through_model_instances = [
UserRoleRelationship(
role=instance,
user=user,
tenant_id=tenant_id,
)
for user in users
]
UserRoleRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):
resource_type = serializers.CharField(source="type")
id = serializers.UUIDField()
class JSONAPIMeta:
resource_name = "provider-group-identifier"
def to_representation(self, instance):
"""
Ensure 'type' is used in the output instead of 'resource_type'.
"""
representation = super().to_representation(instance)
representation["type"] = representation.pop("resource_type", None)
return representation
def to_internal_value(self, data):
"""
Map 'type' back to 'resource_type' during input.
"""
data["resource_type"] = data.pop("type", None)
return super().to_internal_value(data)
class RoleProviderGroupRelationshipSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for modifying role memberships
"""
provider_groups = serializers.ListField(
child=ProviderGroupResourceIdentifierSerializer(),
help_text="List of resource identifier objects representing provider groups.",
)
def create(self, validated_data):
provider_group_ids = [item["id"] for item in validated_data["provider_groups"]]
provider_groups = ProviderGroup.objects.filter(id__in=provider_group_ids)
tenant_id = self.context.get("tenant_id")
new_relationships = [
RoleProviderGroupRelationship(
role=self.context.get("role"), provider_group=pg, tenant_id=tenant_id
)
for pg in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(new_relationships)
return self.context.get("role")
def update(self, instance, validated_data):
provider_group_ids = [item["id"] for item in validated_data["provider_groups"]]
provider_groups = ProviderGroup.objects.filter(id__in=provider_group_ids)
tenant_id = self.context.get("tenant_id")
instance.provider_groups.clear()
new_relationships = [
RoleProviderGroupRelationship(
role=instance, provider_group=pg, tenant_id=tenant_id
)
for pg in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(new_relationships)
return instance
class Meta:
model = RoleProviderGroupRelationship
fields = ["id", "provider_groups"]
# Compliance overview
@@ -1265,7 +1744,7 @@ class OverviewProviderSerializer(serializers.Serializer):
"properties": {
"pass": {"type": "integer"},
"fail": {"type": "integer"},
"manual": {"type": "integer"},
"muted": {"type": "integer"},
"total": {"type": "integer"},
},
}
@@ -1274,7 +1753,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"],
}
@@ -1334,6 +1813,24 @@ class OverviewSeveritySerializer(serializers.Serializer):
return {"version": "v1"}
class OverviewServiceSerializer(serializers.Serializer):
id = serializers.CharField(source="service")
total = serializers.IntegerField()
_pass = serializers.IntegerField()
fail = serializers.IntegerField()
muted = serializers.IntegerField()
class JSONAPIMeta:
resource_name = "services-overview"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["pass"] = self.fields.pop("_pass")
def get_root_meta(self, _resource, _many):
return {"version": "v1"}
# Schedules

View File

@@ -11,16 +11,20 @@ from api.v1.views import (
InvitationViewSet,
MembershipViewSet,
OverviewViewSet,
ProviderGroupProvidersRelationshipView,
ProviderGroupViewSet,
ProviderSecretViewSet,
ProviderViewSet,
ResourceViewSet,
RoleProviderGroupRelationshipView,
RoleViewSet,
ScanViewSet,
ScheduleViewSet,
SchemaView,
TaskViewSet,
TenantMembersViewSet,
TenantViewSet,
UserRoleRelationshipView,
UserViewSet,
)
@@ -29,11 +33,12 @@ router = routers.DefaultRouter(trailing_slash=False)
router.register(r"users", UserViewSet, basename="user")
router.register(r"tenants", TenantViewSet, basename="tenant")
router.register(r"providers", ProviderViewSet, basename="provider")
router.register(r"provider_groups", ProviderGroupViewSet, basename="providergroup")
router.register(r"provider-groups", ProviderGroupViewSet, basename="providergroup")
router.register(r"scans", ScanViewSet, basename="scan")
router.register(r"tasks", TaskViewSet, basename="task")
router.register(r"resources", ResourceViewSet, basename="resource")
router.register(r"findings", FindingViewSet, basename="finding")
router.register(r"roles", RoleViewSet, basename="role")
router.register(
r"compliance-overviews", ComplianceOverviewViewSet, basename="complianceoverview"
)
@@ -80,6 +85,27 @@ urlpatterns = [
InvitationAcceptViewSet.as_view({"post": "accept"}),
name="invitation-accept",
),
path(
"roles/<uuid:pk>/relationships/provider_groups",
RoleProviderGroupRelationshipView.as_view(
{"post": "create", "patch": "partial_update", "delete": "destroy"}
),
name="role-provider-groups-relationship",
),
path(
"users/<uuid:pk>/relationships/roles",
UserRoleRelationshipView.as_view(
{"post": "create", "patch": "partial_update", "delete": "destroy"}
),
name="user-roles-relationship",
),
path(
"provider-groups/<uuid:pk>/relationships/providers",
ProviderGroupProvidersRelationshipView.as_view(
{"post": "create", "patch": "partial_update", "delete": "destroy"}
),
name="provider_group-providers-relationship",
),
path("", include(router.urls)),
path("", include(tenants_router.urls)),
path("", include(users_router.urls)),

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,21 @@
from celery import Celery, Task
from config.env import env
BROKER_VISIBILITY_TIMEOUT = env.int("DJANGO_BROKER_VISIBILITY_TIMEOUT", default=86400)
celery_app = Celery("tasks")
celery_app.config_from_object("django.conf:settings", namespace="CELERY")
celery_app.conf.update(result_extended=True, result_expires=None)
celery_app.conf.broker_transport_options = {
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
}
celery_app.conf.result_backend_transport_options = {
"visibility_timeout": BROKER_VISIBILITY_TIMEOUT
}
celery_app.conf.visibility_timeout = BROKER_VISIBILITY_TIMEOUT
celery_app.autodiscover_tasks(["api"])
@@ -35,10 +46,10 @@ class RLSTask(Task):
**options,
)
task_result_instance = TaskResult.objects.get(task_id=result.task_id)
from api.db_utils import tenant_transaction
from api.db_utils import rls_transaction
tenant_id = kwargs.get("tenant_id")
with tenant_transaction(tenant_id):
with rls_transaction(tenant_id):
APITask.objects.create(
id=task_result_instance.task_id,
tenant_id=tenant_id,

View File

@@ -1,7 +1,6 @@
from config.django.base import * # noqa
from config.env import env
DEBUG = env.bool("DJANGO_DEBUG", default=True)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])

View File

@@ -1,7 +1,6 @@
from config.django.base import * # noqa
from config.env import env
DEBUG = env.bool("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])

View File

@@ -1,7 +1,6 @@
from config.django.base import * # noqa
from config.env import env
DEBUG = env.bool("DJANGO_DEBUG", default=False)
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
@@ -10,8 +9,8 @@ DATABASES = {
"default": {
"ENGINE": "psqlextra.backend",
"NAME": "prowler_db_test",
"USER": env("POSTGRES_USER", default="prowler"),
"PASSWORD": env("POSTGRES_PASSWORD", default="S3cret"),
"USER": env("POSTGRES_USER", default="prowler_admin"),
"PASSWORD": env("POSTGRES_PASSWORD", default="postgres"),
"HOST": env("POSTGRES_HOST", default="localhost"),
"PORT": env("POSTGRES_PORT", default="5432"),
},

View File

@@ -1,35 +1,39 @@
import logging
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from django.conf import settings
from datetime import datetime, timezone, timedelta
from django.db import connections as django_connections, connection as django_connection
from django.db import connection as django_connection
from django.db import connections as django_connections
from django.urls import reverse
from django_celery_results.models import TaskResult
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
from rest_framework import status
from rest_framework.test import APIClient
from api.db_utils import rls_transaction
from api.models import (
ComplianceOverview,
Finding,
)
from api.models import (
User,
Invitation,
Membership,
Provider,
ProviderGroup,
ProviderSecret,
Resource,
ResourceTag,
Role,
Scan,
ScanSummary,
StateChoices,
Task,
Membership,
ProviderSecret,
Invitation,
ComplianceOverview,
User,
UserRoleRelationship,
)
from api.rls import Tenant
from api.v1.serializers import TokenSerializer
from prowler.lib.check.models import Severity
from prowler.lib.outputs.finding import Status
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
NO_TENANT_HTTP_STATUS = status.HTTP_401_UNAUTHORIZED
@@ -83,8 +87,150 @@ def create_test_user(django_db_setup, django_db_blocker):
return user
@pytest.fixture(scope="function")
def create_test_user_rbac(django_db_setup, django_db_blocker, tenants_fixture):
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing",
email="rbac@rbac.com",
password=TEST_PASSWORD,
)
tenant = tenants_fixture[0]
Membership.objects.create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.OWNER,
)
Role.objects.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.create(
user=user,
role=Role.objects.get(name="admin"),
tenant_id=tenant.id,
)
return user
@pytest.fixture(scope="function")
def create_test_user_rbac_no_roles(django_db_setup, django_db_blocker, tenants_fixture):
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing",
email="rbac_noroles@rbac.com",
password=TEST_PASSWORD,
)
tenant = tenants_fixture[0]
Membership.objects.create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.OWNER,
)
return user
@pytest.fixture(scope="function")
def create_test_user_rbac_limited(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
user = User.objects.create_user(
name="testing_limited",
email="rbac_limited@rbac.com",
password=TEST_PASSWORD,
)
tenant = Tenant.objects.create(
name="Tenant Test",
)
Membership.objects.create(
user=user,
tenant=tenant,
role=Membership.RoleChoices.OWNER,
)
Role.objects.create(
name="limited",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
UserRoleRelationship.objects.create(
user=user,
role=Role.objects.get(name="limited"),
tenant_id=tenant.id,
)
return user
@pytest.fixture
def authenticated_client(create_test_user, tenants_fixture, client):
def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client):
client.user = create_test_user_rbac
tenant_id = tenants_fixture[0].id
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "rbac@rbac.com",
"password": TEST_PASSWORD,
"tenant_id": tenant_id,
}
)
serializer.is_valid(raise_exception=True)
access_token = serializer.validated_data["access"]
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
return client
@pytest.fixture
def authenticated_client_rbac_noroles(
create_test_user_rbac_no_roles, tenants_fixture, client
):
client.user = create_test_user_rbac_no_roles
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "rbac_noroles@rbac.com",
"password": TEST_PASSWORD,
}
)
serializer.is_valid()
access_token = serializer.validated_data["access"]
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
return client
@pytest.fixture
def authenticated_client_no_permissions_rbac(
create_test_user_rbac_limited, tenants_fixture, client
):
client.user = create_test_user_rbac_limited
serializer = TokenSerializer(
data={
"type": "tokens",
"email": "rbac_limited@rbac.com",
"password": TEST_PASSWORD,
}
)
serializer.is_valid()
access_token = serializer.validated_data["access"]
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
return client
@pytest.fixture
def authenticated_client(
create_test_user, tenants_fixture, set_user_admin_roles_fixture, client
):
client.user = create_test_user
serializer = TokenSerializer(
data={"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD}
@@ -104,6 +250,7 @@ def authenticated_api_client(create_test_user, tenants_fixture):
serializer.is_valid()
access_token = serializer.validated_data["access"]
client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}"
return client
@@ -128,13 +275,37 @@ def tenants_fixture(create_test_user):
tenant3 = Tenant.objects.create(
name="Tenant Three",
)
return tenant1, tenant2, tenant3
@pytest.fixture
def set_user_admin_roles_fixture(create_test_user, tenants_fixture):
user = create_test_user
for tenant in tenants_fixture[:2]:
with rls_transaction(str(tenant.id)):
role = Role.objects.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.create(
user=user,
role=role,
tenant_id=tenant.id,
)
@pytest.fixture
def invitations_fixture(create_test_user, tenants_fixture):
user = create_test_user
*_, tenant = tenants_fixture
tenant = tenants_fixture[0]
valid_invitation = Invitation.objects.create(
email="testing@prowler.com",
state=Invitation.State.PENDING,
@@ -153,6 +324,20 @@ def invitations_fixture(create_test_user, tenants_fixture):
return valid_invitation, expired_invitation
@pytest.fixture
def users_fixture(django_user_model):
user1 = User.objects.create_user(
name="user1", email="test_unit0@prowler.com", password="S3cret"
)
user2 = User.objects.create_user(
name="user2", email="test_unit1@prowler.com", password="S3cret"
)
user3 = User.objects.create_user(
name="user3", email="test_unit2@prowler.com", password="S3cret"
)
return user1, user2, user3
@pytest.fixture
def providers_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
@@ -210,6 +395,74 @@ def provider_groups_fixture(tenants_fixture):
return pgroup1, pgroup2, pgroup3
@pytest.fixture
def admin_role_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
return Role.objects.get_or_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,
)[0]
@pytest.fixture
def roles_fixture(tenants_fixture):
tenant, *_ = tenants_fixture
role1 = Role.objects.create(
name="Role One",
tenant_id=tenant.id,
manage_users=True,
manage_account=True,
manage_billing=True,
manage_providers=True,
manage_integrations=False,
manage_scans=True,
unlimited_visibility=False,
)
role2 = Role.objects.create(
name="Role Two",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=True,
manage_integrations=True,
manage_scans=True,
unlimited_visibility=True,
)
role3 = Role.objects.create(
name="Role Three",
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,
)
role4 = Role.objects.create(
name="Role Four",
tenant_id=tenant.id,
manage_users=False,
manage_account=False,
manage_billing=False,
manage_providers=False,
manage_integrations=False,
manage_scans=False,
unlimited_visibility=False,
)
return role1, role2, role3, role4
@pytest.fixture
def provider_secret_fixture(providers_fixture):
return tuple(
@@ -373,6 +626,7 @@ def findings_fixture(scans_fixture, resources_fixture):
"CheckId": "test_check_id",
"Description": "test description apple sauce",
},
first_seen_at="2024-01-02T00:00:00Z",
)
finding1.add_resources([resource1])
@@ -398,6 +652,7 @@ def findings_fixture(scans_fixture, resources_fixture):
"CheckId": "test_check_id",
"Description": "test description orange juice",
},
first_seen_at="2024-01-02T00:00:00Z",
)
finding2.add_resources([resource2])
@@ -537,10 +792,107 @@ def get_api_tokens(
data=json_body,
format="vnd.api+json",
)
return response.json()["data"]["attributes"]["access"], response.json()["data"][
"attributes"
]["refresh"]
return (
response.json()["data"]["attributes"]["access"],
response.json()["data"]["attributes"]["refresh"],
)
@pytest.fixture
def scan_summaries_fixture(tenants_fixture, providers_fixture):
tenant = tenants_fixture[0]
provider = providers_fixture[0]
scan = Scan.objects.create(
name="overview scan",
provider=provider,
trigger=Scan.TriggerChoices.MANUAL,
state=StateChoices.COMPLETED,
tenant=tenant,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check1",
service="service1",
severity="high",
region="region1",
_pass=1,
fail=0,
muted=0,
total=1,
new=1,
changed=0,
unchanged=0,
fail_new=0,
fail_changed=0,
pass_new=1,
pass_changed=0,
muted_new=0,
muted_changed=0,
scan=scan,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check1",
service="service1",
severity="high",
region="region2",
_pass=0,
fail=1,
muted=1,
total=2,
new=2,
changed=0,
unchanged=0,
fail_new=1,
fail_changed=0,
pass_new=0,
pass_changed=0,
muted_new=1,
muted_changed=0,
scan=scan,
)
ScanSummary.objects.create(
tenant=tenant,
check_id="check2",
service="service2",
severity="critical",
region="region1",
_pass=1,
fail=0,
muted=0,
total=1,
new=1,
changed=0,
unchanged=0,
fail_new=0,
fail_changed=0,
pass_new=1,
pass_changed=0,
muted_new=0,
muted_changed=0,
scan=scan,
)
def get_authorization_header(access_token: str) -> dict:
return {"Authorization": f"Bearer {access_token}"}
def pytest_collection_modifyitems(items):
"""Ensure test_rbac.py is executed first."""
items.sort(key=lambda item: 0 if "test_rbac.py" in item.nodeid else 1)
def pytest_configure(config):
# Apply the mock before the test session starts. This is necessary to avoid admin error when running the
# 0004_rbac_missing_admin_roles migration
patch("api.db_router.MainRouter.admin_db", new="default").start()
def pytest_unconfigure(config):
# Stop all patches after the test session ends. This is necessary to avoid admin error when running the
# 0004_rbac_missing_admin_roles migration
patch.stopall()

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,
},
)

View File

@@ -1,8 +1,9 @@
from celery.utils.log import get_task_logger
from django.db import transaction
from api.db_utils import batch_delete
from api.models import Finding, Provider, Resource, Scan, ScanSummary
from api.db_router import MainRouter
from api.db_utils import batch_delete, rls_transaction
from api.models import Finding, Provider, Resource, Scan, ScanSummary, Tenant
logger = get_task_logger(__name__)
@@ -49,3 +50,26 @@ def delete_provider(pk: str):
deletion_summary.update(provider_summary)
return deletion_summary
def delete_tenant(pk: str):
"""
Gracefully deletes an instance of a tenant along with its related data.
Args:
pk (str): The primary key of the Tenant instance to delete.
Returns:
dict: A dictionary with the count of deleted objects per model,
including related models.
"""
deletion_summary = {}
for provider in Provider.objects.using(MainRouter.admin_db).filter(tenant_id=pk):
with rls_transaction(pk):
summary = delete_provider(provider.id)
deletion_summary.update(summary)
Tenant.objects.using(MainRouter.admin_db).filter(id=pk).delete()
return deletion_summary

View File

@@ -1,8 +1,13 @@
import os
import time
import zipfile
from copy import deepcopy
from datetime import datetime, timezone
import boto3
from botocore.exceptions import ClientError
from celery.utils.log import get_task_logger
from config.env import env
from config.settings.celery import CELERY_DEADLOCK_ATTEMPTS
from django.db import IntegrityError, OperationalError
from django.db.models import Case, Count, IntegerField, Sum, When
@@ -11,7 +16,7 @@ from api.compliance import (
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
generate_scan_compliance,
)
from api.db_utils import tenant_transaction
from api.db_utils import rls_transaction
from api.models import (
ComplianceOverview,
Finding,
@@ -25,15 +30,117 @@ from api.models import (
from api.models import StatusChoices as FindingStatus
from api.utils import initialize_prowler_provider
from api.v1.serializers import ScanTaskSerializer
from prowler.config.config import (
csv_file_suffix,
html_file_suffix,
json_asff_file_suffix,
json_ocsf_file_suffix,
output_file_timestamp,
tmp_output_directory,
)
from prowler.lib.outputs.asff.asff import ASFF
from prowler.lib.outputs.csv.csv import CSV
from prowler.lib.outputs.finding import Finding as ProwlerFinding
from prowler.lib.outputs.html.html import HTML
from prowler.lib.outputs.ocsf.ocsf import OCSF
from prowler.lib.scan.scan import Scan as ProwlerScan
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-asff": {"class": ASFF, "suffix": json_asff_file_suffix, "kwargs": {}},
"json-ocsf": {"class": OCSF, "suffix": json_ocsf_file_suffix, "kwargs": {}},
"html": {"class": HTML, "suffix": html_file_suffix, "kwargs": {"stats": {}}},
}
# Mapping provider types to their identity components for output paths
PROVIDER_IDENTITY_MAP = {
"aws": lambda p: p.identity.account,
"azure": lambda p: p.identity.tenant_domain,
"gcp": lambda p: p.identity.profile,
"kubernetes": lambda p: p.identity.context.replace(":", "_").replace("/", "_"),
}
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"artifacts/{output_directory.split('/')[-1]}{suffix}",
)
return zip_path
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 env.str("ARTIFACTS_AWS_S3_OUTPUT_BUCKET", ""):
return
if env.str("ARTIFACTS_AWS_ACCESS_KEY_ID", ""):
s3 = boto3.client(
"s3",
aws_access_key_id=env.str("ARTIFACTS_AWS_ACCESS_KEY_ID"),
aws_secret_access_key=env.str("ARTIFACTS_AWS_SECRET_ACCESS_KEY"),
aws_session_token=env.str("ARTIFACTS_AWS_SESSION_TOKEN"),
region_name=env.str("ARTIFACTS_AWS_DEFAULT_REGION"),
)
else:
s3 = boto3.client("s3")
s3_key = f"{tenant_id}/{scan_id}/{os.path.basename(zip_path)}"
try:
s3.upload_file(
Filename=zip_path,
Bucket=env.str("ARTIFACTS_AWS_S3_OUTPUT_BUCKET"),
Key=s3_key,
)
return f"s3://{env.str("ARTIFACTS_AWS_S3_OUTPUT_BUCKET")}/{s3_key}"
except ClientError as e:
logger.error(f"S3 upload failed: {str(e)}")
raise e
def _create_finding_delta(
last_status: FindingStatus | None | str, new_status: FindingStatus | None
) -> Finding.DeltaChoices:
) -> Finding.DeltaChoices | None:
"""
Determine the delta status of a finding based on its previous and current status.
@@ -53,7 +160,11 @@ def _create_finding_delta(
def _store_resources(
finding: ProwlerFinding, tenant_id: str, provider_instance: Provider
finding: ProwlerFinding,
tenant_id: str,
provider_instance: Provider,
resource_cache: dict,
tag_cache: dict,
) -> tuple[Resource, tuple[str, str]]:
"""
Store resource information from a finding, including tags, in the database.
@@ -65,40 +176,91 @@ def _store_resources(
Returns:
tuple:
- Resource: The resource instance created or retrieved from the database.
- Resource: The resource instance created or updated from the database.
- tuple[str, str]: A tuple containing the resource UID and region.
"""
with tenant_transaction(tenant_id):
resource_instance, created = Resource.objects.get_or_create(
tenant_id=tenant_id,
provider=provider_instance,
uid=finding.resource_uid,
defaults={
"region": finding.region,
"service": finding.service_name,
"type": finding.resource_type,
},
)
resource_uid = finding.resource_uid
if not created:
resource_instance.region = finding.region
resource_instance.service = finding.service_name
resource_instance.type = finding.resource_type
resource_instance.save()
with tenant_transaction(tenant_id):
tags = [
ResourceTag.objects.get_or_create(
tenant_id=tenant_id, key=key, value=value
)[0]
for key, value in finding.resource_tags.items()
]
# Check cache or create/update resource
if resource_uid in resource_cache:
resource_instance = resource_cache[resource_uid]
update_fields = []
for field, value in [
("region", finding.region),
("service", finding.service_name),
("type", finding.resource_type),
("name", finding.resource_name),
]:
if getattr(resource_instance, field) != value:
setattr(resource_instance, field, value)
update_fields.append(field)
if update_fields:
with rls_transaction(tenant_id):
resource_instance.save(update_fields=update_fields)
else:
with rls_transaction(tenant_id):
resource_instance, _ = Resource.objects.update_or_create(
tenant_id=tenant_id,
provider=provider_instance,
uid=resource_uid,
defaults={
"region": finding.region,
"service": finding.service_name,
"type": finding.resource_type,
"name": finding.resource_name,
},
)
resource_cache[resource_uid] = resource_instance
# Process tags with caching
tags = []
for key, value in finding.resource_tags.items():
tag_key = (key, value)
if tag_key not in tag_cache:
with rls_transaction(tenant_id):
tag_instance, _ = ResourceTag.objects.get_or_create(
tenant_id=tenant_id, key=key, value=value
)
tag_cache[tag_key] = tag_instance
tags.append(tag_cache[tag_key])
with rls_transaction(tenant_id):
resource_instance.upsert_or_delete_tags(tags=tags)
return resource_instance, (resource_instance.uid, resource_instance.region)
def _generate_output_directory(
prowler_provider: object, tenant_id: str, scan_id: str
) -> str:
"""
Generate a dynamic output directory path based on the given provider type.
Args:
prowler_provider (object): An object that has a `type` attribute indicating
the provider type (e.g., "aws", "azure", etc.).
tenant_id (str): A unique identifier for the tenant. Used to build the output path.
scan_id (str): A unique identifier for the scan. Included in the output path.
Returns:
str: The complete path to the output directory, including the tenant ID, scan ID,
provider identity, and a timestamp.
"""
provider_type = prowler_provider.type
get_identity = PROVIDER_IDENTITY_MAP.get(provider_type, lambda _: "unknown")
return (
f"{tmp_output_directory}/{tenant_id}/{scan_id}/prowler-output-"
f"{get_identity(prowler_provider)}-{output_file_timestamp}"
)
def perform_prowler_scan(
tenant_id: str, scan_id: str, provider_id: str, checks_to_execute: list[str] = None
tenant_id: str,
scan_id: str,
provider_id: str,
checks_to_execute: list[str] = None,
):
"""
Perform a scan using Prowler and store the findings and resources in the database.
@@ -116,13 +278,15 @@ def perform_prowler_scan(
ValueError: If the provider cannot be connected.
"""
generate_compliance = False
check_status_by_region = {}
exception = None
unique_resources = set()
start_time = time.time()
resource_cache = {}
tag_cache = {}
last_status_cache = {}
with tenant_transaction(tenant_id):
with rls_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
scan_instance = Scan.objects.get(pk=scan_id)
scan_instance.state = StateChoices.EXECUTING
@@ -130,7 +294,8 @@ def perform_prowler_scan(
scan_instance.save()
try:
with tenant_transaction(tenant_id):
# Provider initialization
with rls_transaction(tenant_id):
try:
prowler_provider = initialize_prowler_provider(provider_instance)
provider_instance.connected = True
@@ -145,102 +310,66 @@ def perform_prowler_scan(
)
provider_instance.save()
generate_compliance = provider_instance.provider != Provider.ProviderChoices.GCP
prowler_scan = ProwlerScan(provider=prowler_provider, checks=checks_to_execute)
# Scan configuration
prowler_scan = ProwlerScan(
provider=prowler_provider, checks=checks_to_execute or []
)
output_directory = _generate_output_directory(
prowler_provider, tenant_id, scan_id
)
# Create the output directory
os.makedirs("/".join(output_directory.split("/")[:-1]), exist_ok=True)
resource_cache = {}
tag_cache = {}
last_status_cache = {}
for progress, findings in prowler_scan.scan():
all_findings = []
for progress, findings, stats 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 tenant_transaction(tenant_id):
# Process resource
resource_uid = finding.resource_uid
if resource_uid not in resource_cache:
# Get or create the resource
resource_instance, _ = Resource.objects.get_or_create(
tenant_id=tenant_id,
provider=provider_instance,
uid=resource_uid,
defaults={
"region": finding.region,
"service": finding.service_name,
"type": finding.resource_type,
"name": finding.resource_name,
},
)
resource_cache[resource_uid] = resource_instance
else:
resource_instance = resource_cache[resource_uid]
# Update resource fields if necessary
updated_fields = []
if resource_instance.region != finding.region:
resource_instance.region = finding.region
updated_fields.append("region")
if resource_instance.service != finding.service_name:
resource_instance.service = finding.service_name
updated_fields.append("service")
if resource_instance.type != finding.resource_type:
resource_instance.type = finding.resource_type
updated_fields.append("type")
if updated_fields:
with tenant_transaction(tenant_id):
resource_instance.save(update_fields=updated_fields)
resource_instance, resource_uid_region = _store_resources(
finding,
tenant_id,
provider_instance,
resource_cache,
tag_cache,
)
unique_resources.add(resource_uid_region)
break
except (OperationalError, IntegrityError) as db_err:
if attempt < CELERY_DEADLOCK_ATTEMPTS - 1:
logger.warning(
f"{'Deadlock error' if isinstance(db_err, OperationalError) else 'Integrity error'} "
f"detected when processing resource {resource_uid} on scan {scan_id}. Retrying..."
f"Database error ({type(db_err).__name__}) "
f"processing resource {finding.resource_uid}, retrying..."
)
time.sleep(0.1 * (2**attempt))
continue
else:
raise db_err
# Update tags
tags = []
with tenant_transaction(tenant_id):
for key, value in finding.resource_tags.items():
tag_key = (key, value)
if tag_key not in tag_cache:
tag_instance, _ = ResourceTag.objects.get_or_create(
tenant_id=tenant_id, key=key, value=value
)
tag_cache[tag_key] = tag_instance
else:
tag_instance = tag_cache[tag_key]
tags.append(tag_instance)
resource_instance.upsert_or_delete_tags(tags=tags)
unique_resources.add((resource_instance.uid, resource_instance.region))
# Process finding
with tenant_transaction(tenant_id):
# Finding processing
with rls_transaction(tenant_id):
finding_uid = finding.uid
if finding_uid not in last_status_cache:
most_recent_finding = (
most_recent = (
Finding.objects.filter(uid=finding_uid)
.order_by("-id")
.values("status")
.order_by("-inserted_at")
.values("status", "first_seen_at")
.first()
)
last_status = (
most_recent_finding["status"]
if most_recent_finding
else None
last_status, first_seen = (
(most_recent["status"], most_recent["first_seen_at"])
if most_recent
else (None, None)
)
last_status_cache[finding_uid] = last_status
last_status_cache[finding_uid] = (last_status, first_seen)
else:
last_status = last_status_cache[finding_uid]
last_status, first_seen = last_status_cache[finding_uid]
status = FindingStatus[finding.status]
delta = _create_finding_delta(last_status, status)
first_seen = first_seen or datetime.now(tz=timezone.utc)
# Create the finding
finding_instance = Finding.objects.create(
tenant_id=tenant_id,
uid=finding_uid,
@@ -253,91 +382,96 @@ def perform_prowler_scan(
raw_result=finding.raw,
check_id=finding.check_id,
scan=scan_instance,
first_seen_at=first_seen,
)
finding_instance.add_resources([resource_instance])
# Update compliance data if applicable
if not generate_compliance or finding.status.value == "MUTED":
continue
# Update compliance status
if finding.status.value != "MUTED":
region_data = check_status_by_region.setdefault(finding.region, {})
if region_data.get(finding.check_id) != "FAIL":
region_data[finding.check_id] = finding.status.value
region_dict = check_status_by_region.setdefault(finding.region, {})
current_status = region_dict.get(finding.check_id)
if current_status == "FAIL":
continue
region_dict[finding.check_id] = finding.status.value
# Update scan progress
with tenant_transaction(tenant_id):
# Progress updates and output generation
with rls_transaction(tenant_id):
scan_instance.progress = progress
scan_instance.save()
all_findings.extend(findings)
# Generate output files
for mode, config in OUTPUT_FORMATS_MAPPING.items():
kwargs = dict(config["kwargs"])
if mode == "html":
kwargs["provider"] = prowler_provider
kwargs["stats"] = stats
config["class"](
findings=all_findings,
create_file_descriptor=True,
file_path=output_directory,
file_extension=config["suffix"],
).batch_write_data_to_file(**kwargs)
scan_instance.state = StateChoices.COMPLETED
# Compress output files
zip_path = _compress_output_files(output_directory)
# Save to configured storage
_upload_to_s3(tenant_id, zip_path, scan_id)
except Exception as e:
logger.error(f"Error performing scan {scan_id}: {e}")
logger.error(f"Scan {scan_id} failed: {str(e)}")
exception = e
scan_instance.state = StateChoices.FAILED
finally:
with tenant_transaction(tenant_id):
# Final scan updates
with rls_transaction(tenant_id):
scan_instance.duration = time.time() - start_time
scan_instance.completed_at = datetime.now(tz=timezone.utc)
scan_instance.unique_resource_count = len(unique_resources)
scan_instance.save()
if exception is None and generate_compliance:
try:
regions = prowler_provider.get_regions()
except AttributeError:
regions = set()
# Compliance processing
if not exception:
compliance_template = PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE[
provider_instance.provider
]
compliance_overview_by_region = {
region: deepcopy(compliance_template) for region in regions
compliance_overview = {
region: deepcopy(compliance_template)
for region in getattr(prowler_provider, "get_regions", lambda: set())()
}
for region, check_status in check_status_by_region.items():
compliance_data = compliance_overview_by_region.setdefault(
region, deepcopy(compliance_template)
)
for check_name, status in check_status.items():
for region, checks in check_status_by_region.items():
for check_id, status in checks.items():
generate_scan_compliance(
compliance_data,
compliance_overview.setdefault(
region, deepcopy(compliance_template)
),
provider_instance.provider,
check_name,
check_id,
status,
)
# Prepare compliance overview objects
compliance_overview_objects = []
for region, compliance_data in compliance_overview_by_region.items():
for compliance_id, compliance in compliance_data.items():
compliance_overview_objects.append(
ComplianceOverview(
tenant_id=tenant_id,
scan=scan_instance,
region=region,
compliance_id=compliance_id,
framework=compliance["framework"],
version=compliance["version"],
description=compliance["description"],
requirements=compliance["requirements"],
requirements_passed=compliance["requirements_status"]["passed"],
requirements_failed=compliance["requirements_status"]["failed"],
requirements_manual=compliance["requirements_status"]["manual"],
total_requirements=compliance["total_requirements"],
)
ComplianceOverview.objects.bulk_create(
[
ComplianceOverview(
tenant_id=tenant_id,
scan=scan_instance,
region=region,
compliance_id=compliance_id,
**compliance_data,
)
with tenant_transaction(tenant_id):
ComplianceOverview.objects.bulk_create(compliance_overview_objects)
for region, data in compliance_overview.items()
for compliance_id, compliance_data in data.items()
]
)
if exception is not None:
if exception:
raise exception
serializer = ScanTaskSerializer(instance=scan_instance)
return serializer.data
return ScanTaskSerializer(instance=scan_instance).data
def aggregate_findings(tenant_id: str, scan_id: str):
@@ -368,8 +502,8 @@ def aggregate_findings(tenant_id: str, scan_id: str):
- muted_new: Muted findings with a delta of 'new'.
- muted_changed: Muted findings with a delta of 'changed'.
"""
with tenant_transaction(tenant_id):
findings = Finding.objects.filter(scan_id=scan_id)
with rls_transaction(tenant_id):
findings = Finding.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
aggregation = findings.values(
"check_id",
@@ -464,29 +598,28 @@ def aggregate_findings(tenant_id: str, scan_id: str):
),
)
with tenant_transaction(tenant_id):
scan_aggregations = {
ScanSummary(
tenant_id=tenant_id,
scan_id=scan_id,
check_id=agg["check_id"],
service=agg["resources__service"],
severity=agg["severity"],
region=agg["resources__region"],
fail=agg["fail"],
_pass=agg["_pass"],
muted=agg["muted"],
total=agg["total"],
new=agg["new"],
changed=agg["changed"],
unchanged=agg["unchanged"],
fail_new=agg["fail_new"],
fail_changed=agg["fail_changed"],
pass_new=agg["pass_new"],
pass_changed=agg["pass_changed"],
muted_new=agg["muted_new"],
muted_changed=agg["muted_changed"],
)
for agg in aggregation
}
ScanSummary.objects.bulk_create(scan_aggregations, batch_size=3000)
ScanSummary.objects.bulk_create(
[
ScanSummary(
tenant_id=tenant_id,
scan_id=scan_id,
check_id=agg["check_id"],
service=agg["resources__service"],
severity=agg["severity"],
region=agg["resources__region"],
**{
k: v or 0
for k, v in agg.items()
if k
not in {
"check_id",
"resources__service",
"severity",
"resources__region",
}
},
)
for agg in aggregation
],
batch_size=3000,
)

View File

@@ -1,15 +1,14 @@
from datetime import datetime, timedelta, timezone
from celery import shared_task
from config.celery import RLSTask
from django_celery_beat.models import PeriodicTask
from tasks.jobs.connection import check_provider_connection
from tasks.jobs.deletion import delete_provider
from tasks.jobs.deletion import delete_provider, delete_tenant
from tasks.jobs.scan import aggregate_findings, perform_prowler_scan
from tasks.utils import get_next_execution_datetime
from api.db_utils import tenant_transaction
from api.db_utils import rls_transaction
from api.decorators import set_tenant
from api.models import Provider, Scan
from api.models import Scan, StateChoices
@shared_task(base=RLSTask, name="provider-connection-check")
@@ -99,29 +98,43 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
"""
task_id = self.request.id
with tenant_transaction(tenant_id):
provider_instance = Provider.objects.get(pk=provider_id)
with rls_transaction(tenant_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,
)
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,
)
perform_scan_summary_task.apply_async(
kwargs={
"tenant_id": tenant_id,
@@ -134,3 +147,8 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
@shared_task(name="scan-summary")
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")
def delete_tenant_task(tenant_id: str):
return delete_tenant(pk=tenant_id)

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={

View File

@@ -1,13 +1,13 @@
import pytest
from django.core.exceptions import ObjectDoesNotExist
from tasks.jobs.deletion import delete_provider
from tasks.jobs.deletion import delete_provider, delete_tenant
from api.models import Provider
from api.models import Provider, Tenant
@pytest.mark.django_db
class TestDeleteInstance:
def test_delete_instance_success(self, providers_fixture):
class TestDeleteProvider:
def test_delete_provider_success(self, providers_fixture):
instance = providers_fixture[0]
result = delete_provider(instance.id)
@@ -15,8 +15,46 @@ class TestDeleteInstance:
with pytest.raises(ObjectDoesNotExist):
Provider.objects.get(pk=instance.id)
def test_delete_instance_does_not_exist(self):
def test_delete_provider_does_not_exist(self):
non_existent_pk = "babf6796-cfcc-4fd3-9dcf-88d012247645"
with pytest.raises(ObjectDoesNotExist):
delete_provider(non_existent_pk)
@pytest.mark.django_db
class TestDeleteTenant:
def test_delete_tenant_success(self, tenants_fixture, providers_fixture):
"""
Test successful deletion of a tenant and its related data.
"""
tenant = tenants_fixture[0]
providers = Provider.objects.filter(tenant_id=tenant.id)
# Ensure the tenant and related providers exist before deletion
assert Tenant.objects.filter(id=tenant.id).exists()
assert providers.exists()
# Call the function and validate the result
deletion_summary = delete_tenant(tenant.id)
assert deletion_summary is not None
assert not Tenant.objects.filter(id=tenant.id).exists()
assert not Provider.objects.filter(tenant_id=tenant.id).exists()
def test_delete_tenant_with_no_providers(self, tenants_fixture):
"""
Test deletion of a tenant with no related providers.
"""
tenant = tenants_fixture[1] # Assume this tenant has no providers
providers = Provider.objects.filter(tenant_id=tenant.id)
# Ensure the tenant exists but has no related providers
assert Tenant.objects.filter(id=tenant.id).exists()
assert not providers.exists()
# Call the function and validate the result
deletion_summary = delete_tenant(tenant.id)
assert deletion_summary == {} # No providers, so empty summary
assert not Tenant.objects.filter(id=tenant.id).exists()

View File

@@ -1,3 +1,4 @@
import uuid
from unittest.mock import MagicMock, patch
import pytest
@@ -26,7 +27,7 @@ class TestPerformScan:
providers_fixture,
):
with (
patch("api.db_utils.tenant_transaction"),
patch("api.db_utils.rls_transaction"),
patch(
"tasks.jobs.scan.initialize_prowler_provider"
) as mock_initialize_prowler_provider,
@@ -165,10 +166,10 @@ class TestPerformScan:
"tasks.jobs.scan.initialize_prowler_provider",
side_effect=Exception("Connection error"),
)
@patch("api.db_utils.tenant_transaction")
@patch("api.db_utils.rls_transaction")
def test_perform_prowler_scan_no_connection(
self,
mock_tenant_transaction,
mock_rls_transaction,
mock_initialize_prowler_provider,
mock_prowler_scan_class,
tenants_fixture,
@@ -205,14 +206,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.tenant_transaction")
@patch("api.db_utils.rls_transaction")
def test_store_resources_new_resource(
self,
mock_tenant_transaction,
mock_rls_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = "tenant123"
tenant_id = uuid.uuid4()
provider_instance = MagicMock()
provider_instance.id = "provider456"
@@ -253,14 +254,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.tenant_transaction")
@patch("api.db_utils.rls_transaction")
def test_store_resources_existing_resource(
self,
mock_tenant_transaction,
mock_rls_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = "tenant123"
tenant_id = uuid.uuid4()
provider_instance = MagicMock()
provider_instance.id = "provider456"
@@ -310,14 +311,14 @@ class TestPerformScan:
@patch("api.models.ResourceTag.objects.get_or_create")
@patch("api.models.Resource.objects.get_or_create")
@patch("api.db_utils.tenant_transaction")
@patch("api.db_utils.rls_transaction")
def test_store_resources_with_tags(
self,
mock_tenant_transaction,
mock_rls_transaction,
mock_get_or_create_resource,
mock_get_or_create_tag,
):
tenant_id = "tenant123"
tenant_id = uuid.uuid4()
provider_instance = MagicMock()
provider_instance.id = "provider456"

View File

@@ -0,0 +1,76 @@
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 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"
)

View File

@@ -0,0 +1,26 @@
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})

11
codecov.yml Normal file
View File

@@ -0,0 +1,11 @@
component_management:
individual_components:
- component_id: "prowler"
paths:
- "prowler/**"
- component_id: "api"
paths:
- "api/**"
comment:
layout: "header, diff, flags, components"

View File

@@ -0,0 +1,301 @@
# AWS SSO to Prowler Automation Script
## Table of Contents
- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Script Overview](#script-overview)
- [Usage](#usage)
- [Troubleshooting](#troubleshooting)
- [Customization](#customization)
- [Security Considerations](#security-considerations)
- [License](#license)
## Introduction
This repository provides a Bash script that automates the process of logging into AWS Single Sign-On (SSO), extracting temporary AWS credentials, and running **Prowler**—a security tool that performs AWS security best practices assessments—inside a Docker container using those credentials.
By following this guide, you can streamline your AWS security assessments, ensuring that you consistently apply best practices across your AWS accounts.
## Prerequisites
Before you begin, ensure that you have the following tools installed and properly configured on your system:
1. **AWS CLI v2**
- AWS SSO support is available from AWS CLI version 2 onwards.
- [Installation Guide](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
2. **jq**
- A lightweight and flexible command-line JSON processor.
- **macOS (Homebrew):**
```bash
brew install jq
```
- **Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install -y jq
```
- **Windows:**
- [Download jq](https://stedolan.github.io/jq/download/)
3. **Docker**
- Ensure Docker is installed and running on your system.
- [Docker Installation Guide](https://docs.docker.com/get-docker/)
4. **AWS SSO Profile Configuration**
- Ensure that you have configured an AWS CLI profile with SSO.
- [Configuring AWS CLI with SSO](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html)
## Setup
1. **Clone the Repository**
```bash
git clone https://github.com/your-username/aws-sso-prowler-automation.git
cd aws-sso-prowler-automation
```
2. **Create the Automation Script**
Create a new Bash script named `run_prowler_sso.sh` and make it executable.
```bash
nano run_prowler_sso.sh
chmod +x run_prowler_sso.sh
```
3. **Add the Script Content**
Paste the following content into `run_prowler_sso.sh`:
4. **Configure AWS SSO Profile**
Ensure that your AWS CLI profile (`twodragon` in this case) is correctly configured for SSO.
```bash
aws configure sso --profile twodragon
```
**Example Configuration Prompts:**
```
SSO session name (Recommended): [twodragon]
SSO start URL [None]: https://twodragon.awsapps.com/start
SSO region [None]: ap-northeast-2
SSO account ID [None]: 123456789012
SSO role name [None]: ReadOnlyAccess
CLI default client region [None]: ap-northeast-2
CLI default output format [None]: json
CLI profile name [twodragon]: twodragon
```
## Script Overview
The `run_prowler_sso.sh` script performs the following actions:
1. **AWS SSO Login:**
- Initiates AWS SSO login for the specified profile.
- Opens the SSO authorization page in the default browser for user authentication.
2. **Extract Temporary Credentials:**
- Locates the most recent SSO cache file containing the `accessToken`.
- Uses `jq` to parse and extract the `accessToken` from the cache file.
- Retrieves the `sso_role_name` and `sso_account_id` from the AWS CLI configuration.
- Obtains temporary AWS credentials (`AccessKeyId`, `SecretAccessKey`, `SessionToken`) using the extracted `accessToken`.
3. **Set Environment Variables:**
- Exports the extracted AWS credentials as environment variables to be used by the Docker container.
4. **Run Prowler:**
- Executes the **Prowler** Docker container, passing the AWS credentials as environment variables for security assessments.
## Usage
1. **Make the Script Executable**
Ensure the script has execute permissions.
```bash
chmod +x run_prowler_sso.sh
```
2. **Run the Script**
Execute the script to start the AWS SSO login process and run Prowler.
```bash
./run_prowler_sso.sh
```
3. **Follow the Prompts**
- A browser window will open prompting you to authenticate via AWS SSO.
- Complete the authentication process in the browser.
- Upon successful login, the script will extract temporary credentials and run Prowler.
4. **Review Prowler Output**
- Prowler will analyze your AWS environment based on the specified checks and output the results directly in the terminal.
## Troubleshooting
If you encounter issues during the script execution, follow these steps to diagnose and resolve them.
### 1. Verify AWS CLI Version
Ensure you are using AWS CLI version 2 or later.
```bash
aws --version
```
**Expected Output:**
```
aws-cli/2.11.10 Python/3.9.12 Darwin/20.3.0 exe/x86_64 prompt/off
```
If you are not using version 2, [install or update AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html).
### 2. Confirm AWS SSO Profile Configuration
Check that the `twodragon` profile is correctly configured.
```bash
aws configure list-profiles
```
**Expected Output:**
```
default
twodragon
```
Review the profile details:
```bash
aws configure get sso_start_url --profile twodragon
aws configure get sso_region --profile twodragon
aws configure get sso_account_id --profile twodragon
aws configure get sso_role_name --profile twodragon
```
Ensure all fields return the correct values.
### 3. Check SSO Cache File
Ensure that the SSO cache file contains a valid `accessToken`.
```bash
cat ~/.aws/sso/cache/*.json
```
**Example Content:**
```json
{
"accessToken": "eyJz93a...k4laUWw",
"expiresAt": "2024-12-22T14:07:55Z",
"clientId": "example-client-id",
"clientSecret": "example-client-secret",
"startUrl": "https://twodragon.awsapps.com/start#"
}
```
If `accessToken` is `null` or missing, retry the AWS SSO login:
```bash
aws sso login --profile twodragon
```
### 4. Validate `jq` Installation
Ensure that `jq` is installed and functioning correctly.
```bash
jq --version
```
**Expected Output:**
```
jq-1.6
```
If `jq` is not installed, install it using the instructions in the [Prerequisites](#prerequisites) section.
### 5. Test Docker Environment Variables
Verify that the Docker container receives the AWS credentials correctly.
```bash
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
toniblyx/prowler /bin/bash -c 'echo $AWS_ACCESS_KEY_ID; echo $AWS_SECRET_ACCESS_KEY; echo $AWS_SESSION_TOKEN'
```
**Expected Output:**
```
ASIA...
wJalrFEMI/K7MDENG/bPxRfiCY...
IQoJb3JpZ2luX2VjEHwaCXVz...
```
Ensure that none of the environment variables are empty.
### 6. Review Script Output
Run the script with debugging enabled to get detailed output.
1. **Enable Debugging in Script**
Add `set -x` for verbose output.
```bash
#!/bin/bash
set -e
set -x
# ... rest of the script ...
```
2. **Run the Script**
```bash
./run_prowler_sso.sh
```
3. **Analyze Output**
Look for any errors or unexpected values in the output to identify where the script is failing.
## Customization
You can modify the script to suit your specific needs, such as:
- **Changing the AWS Profile Name:**
Update the `PROFILE` variable at the top of the script.
```bash
PROFILE="your-profile-name"
```
- **Adding Prowler Options:**
Pass additional options to Prowler for customized checks or output formats.
```bash
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
toniblyx/prowler -c check123 -M json
```
## Security Considerations
- **Handle Credentials Securely:**
- Avoid sharing or exposing your AWS credentials.
- Do not include sensitive information in logs or version control.
- **Script Permissions:**
- Ensure the script file has appropriate permissions to prevent unauthorized access.
```bash
chmod 700 run_prowler_sso.sh
```
- **Environment Variables:**
- Be cautious when exporting credentials as environment variables.
- Consider using more secure methods for credential management if necessary.
## License
This project is licensed under the [MIT License](LICENSE).

View File

@@ -0,0 +1,136 @@
#!/bin/bash
set -e
# Set the profile name
PROFILE="twodragon"
# Set the Prowler output directory
OUTPUT_DIR=~/prowler-output
mkdir -p "$OUTPUT_DIR"
# Set the port for the local web server
WEB_SERVER_PORT=8000
# ----------------------------------------------
# Functions
# ----------------------------------------------
# Function to open the HTML report in the default browser
open_report() {
local report_path="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
open "$report_path"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
xdg-open "$report_path"
elif [[ "$OSTYPE" == "msys" ]]; then
start "" "$report_path"
else
echo "Automatic method to open Prowler HTML report is not supported on this OS."
echo "Please open the report manually at: $report_path"
fi
}
# Function to start a simple HTTP server to host the Prowler reports
start_web_server() {
local directory="$1"
local port="$2"
echo "Starting local web server to host Prowler reports at http://localhost:$port"
echo "Press Ctrl+C to stop the web server."
# Change to the output directory
cd "$directory"
# Start the HTTP server in the foreground
# Python 3 is required
python3 -m http.server "$port"
}
# ----------------------------------------------
# Main Script
# ----------------------------------------------
# AWS SSO Login
echo "Logging into AWS SSO..."
aws sso login --profile "$PROFILE"
# Extract temporary credentials
echo "Extracting temporary credentials..."
# Find the most recently modified SSO cache file
CACHE_FILE=$(ls -t ~/.aws/sso/cache/*.json 2>/dev/null | head -n 1)
echo "Cache File: $CACHE_FILE"
if [ -z "$CACHE_FILE" ]; then
echo "SSO cache file not found. Please ensure AWS SSO login was successful."
exit 1
fi
# Extract accessToken using jq
ACCESS_TOKEN=$(jq -r '.accessToken' "$CACHE_FILE")
echo "Access Token: $ACCESS_TOKEN"
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" == "null" ]; then
echo "Unable to extract accessToken. Please check your SSO login and cache file."
exit 1
fi
# Extract role name and account ID from AWS CLI configuration
ROLE_NAME=$(aws configure get sso_role_name --profile "$PROFILE")
ACCOUNT_ID=$(aws configure get sso_account_id --profile "$PROFILE")
echo "Role Name: $ROLE_NAME"
echo "Account ID: $ACCOUNT_ID"
if [ -z "$ROLE_NAME" ] || [ -z "$ACCOUNT_ID" ]; then
echo "Unable to extract sso_role_name or sso_account_id. Please check your profile configuration."
exit 1
fi
# Obtain temporary credentials using AWS SSO
TEMP_CREDS=$(aws sso get-role-credentials \
--role-name "$ROLE_NAME" \
--account-id "$ACCOUNT_ID" \
--access-token "$ACCESS_TOKEN" \
--profile "$PROFILE")
echo "TEMP_CREDS: $TEMP_CREDS"
# Extract credentials from the JSON response
AWS_ACCESS_KEY_ID=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.accessKeyId')
AWS_SECRET_ACCESS_KEY=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.secretAccessKey')
AWS_SESSION_TOKEN=$(echo "$TEMP_CREDS" | jq -r '.roleCredentials.sessionToken')
# Verify that all credentials were extracted successfully
if [ -z "$AWS_ACCESS_KEY_ID" ] || [ -z "$AWS_SECRET_ACCESS_KEY" ] || [ -z "$AWS_SESSION_TOKEN" ]; then
echo "Unable to extract temporary credentials."
exit 1
fi
# Export AWS credentials as environment variables
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN
echo "AWS credentials have been set."
# Run Prowler in Docker container
echo "Running Prowler Docker container..."
docker run --platform linux/amd64 \
-e AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" \
-e AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" \
-e AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" \
-v "$OUTPUT_DIR":/home/prowler/output \
toniblyx/prowler -M html -M csv -M json-ocsf --output-directory /home/prowler/output --output-filename prowler-output
echo "Prowler has finished running. Reports are saved in $OUTPUT_DIR."
# Open the HTML report in the default browser
REPORT_PATH="$OUTPUT_DIR/prowler-output.html"
echo "Opening Prowler HTML report..."
open_report "$REPORT_PATH" &
# Start the local web server to host the Prowler dashboard
# This will run in the foreground. To run it in the background, append an ampersand (&) at the end of the command.
start_web_server "$OUTPUT_DIR" "$WEB_SERVER_PORT"

View File

@@ -0,0 +1,24 @@
apiVersion: v2
name: prowler-api
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "5.1.1"

View File

@@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "prowler-api.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "prowler-api.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "prowler-api.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "prowler-api.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "prowler-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "prowler-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "prowler-api.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "prowler-api.labels" -}}
helm.sh/chart: {{ include "prowler-api.chart" . }}
{{ include "prowler-api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "prowler-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "prowler-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "prowler-api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "prowler-api.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "prowler-api.fullname" . }}-config
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
data:
config.yaml: |-
{{- toYaml .Values.mainConfig | nindent 4 }}

View File

@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "prowler-api.fullname" . }}
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "prowler-api.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "prowler-api.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "prowler-api.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
{{- range $name,$config := .Values.containers }}
{{- if $config.enabled }}
- name: {{ $name }}
securityContext:
{{- toYaml $config.securityContext | nindent 12 }}
image: "{{ $config.image.repository }}:{{ $config.image.tag | default $.Chart.AppVersion }}"
imagePullPolicy: {{ $config.image.pullPolicy }}
envFrom:
- secretRef:
name: {{ include "prowler-api.fullname" $ }}
command:
{{- toYaml $config.command | nindent 12 }}
{{- if $config.ports }}
ports:
{{- toYaml $config.ports | nindent 12 }}
{{- end }}
livenessProbe:
{{- toYaml $config.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml $config.readinessProbe | nindent 12 }}
resources:
{{- toYaml $config.resources | nindent 12 }}
volumeMounts:
- name: {{ include "prowler-api.fullname" $ }}-config
mountPath: {{ $.Values.releaseConfigRoot }}{{ $.Values.releaseConfigPath }}
subPath: config.yaml
{{- with .volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- end }}
volumes:
- name: {{ include "prowler-api.fullname" . }}-config
configMap:
name: {{ include "prowler-api.fullname" . }}-config
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "prowler-api.fullname" . }}
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- with .pathType }}
pathType: {{ . }}
{{- end }}
backend:
service:
name: {{ include "prowler-api.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "prowler-api.fullname" . }}
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
type: Opaque
data:
{{- range $k, $v := .Values.secrets }}
{{ $k }}: {{ $v | toString | b64enc | quote }}
{{- end }}

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "prowler-api.fullname" . }}
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
{{- range $name,$config := .Values.containers }}
{{- if $config.ports }}
{{- range $p := $config.ports }}
- port: {{ $p.containerPort }}
targetPort: {{ $p.containerPort }}
protocol: TCP
name: {{ $config.name }}
{{- end }}
{{- end }}
{{- end }}
selector:
{{- include "prowler-api.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "prowler-api.serviceAccountName" . }}
labels:
{{- include "prowler-api.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}

View File

@@ -0,0 +1,625 @@
# Default values for prowler-api.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
containers:
prowler-api:
enabled: true
image:
repository: prowlercloud/prowler-api
pullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
command: ["/home/prowler/docker-entrypoint.sh", "prod"]
worker:
enabled: true
image:
repository: prowlercloud/prowler-api
pullPolicy: IfNotPresent
command: ["/home/prowler/docker-entrypoint.sh", "worker"]
worker-beat:
enabled: true
image:
repository: prowlercloud/prowler-api
pullPolicy: IfNotPresent
command: ["../docker-entrypoint.sh", "beat"]
secrets:
POSTGRES_HOST:
POSTGRES_PORT: 5432
POSTGRES_ADMIN_USER:
POSTGRES_ADMIN_PASSWORD:
POSTGRES_USER:
POSTGRES_PASSWORD:
POSTGRES_DB:
# Valkey settings
VALKEY_HOST: valkey-headless
VALKEY_PORT: "6379"
VALKEY_DB: "0"
# Django settings
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1,prowler-api
DJANGO_BIND_ADDRESS: 0.0.0.0
DJANGO_PORT: "8080"
DJANGO_DEBUG: False
DJANGO_SETTINGS_MODULE: config.django.production
# Select one of [ndjson|human_readable]
DJANGO_LOGGING_FORMATTER: human_readable
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
# Applies to both Django and Celery Workers
DJANGO_LOGGING_LEVEL: INFO
# Defaults to the maximum available based on CPU cores if not set.
DJANGO_WORKERS: 2
# Token lifetime is in minutes
DJANGO_ACCESS_TOKEN_LIFETIME: "30"
# Token lifetime is in minutes
DJANGO_REFRESH_TOKEN_LIFETIME: "1440"
DJANGO_CACHE_MAX_AGE: "3600"
DJANGO_STALE_WHILE_REVALIDATE: "60"
DJANGO_MANAGE_DB_PARTITIONS: "False"
# openssl genrsa -out private.pem 2048
DJANGO_TOKEN_SIGNING_KEY:
# openssl rsa -in private.pem -pubout -out public.pem
DJANGO_TOKEN_VERIFYING_KEY:
# openssl rand -base64 32
DJANGO_SECRETS_ENCRYPTION_KEY:
DJANGO_BROKER_VISIBILITY_TIMEOUT: 86400
releaseConfigRoot: /home/prowler/.cache/pypoetry/virtualenvs/prowler-api-NnJNioq7-py3.12/lib/python3.12/site-packages/
releaseConfigPath: prowler/config/config.yaml
mainConfig:
# AWS Configuration
aws:
# AWS Global Configuration
# aws.mute_non_default_regions --> Set to True to muted failed findings in non-default regions for AccessAnalyzer, GuardDuty, SecurityHub, DRS and Config
mute_non_default_regions: False
# If you want to mute failed findings only in specific regions, create a file with the following syntax and run it with `prowler aws -w mutelist.yaml`:
# Mutelist:
# Accounts:
# "*":
# Checks:
# "*":
# Regions:
# - "ap-southeast-1"
# - "ap-southeast-2"
# Resources:
# - "*"
# AWS IAM Configuration
# aws.iam_user_accesskey_unused --> CIS recommends 45 days
max_unused_access_keys_days: 45
# aws.iam_user_console_access_unused --> CIS recommends 45 days
max_console_access_days: 45
# AWS EC2 Configuration
# aws.ec2_elastic_ip_shodan
# TODO: create common config
shodan_api_key: null
# aws.ec2_securitygroup_with_many_ingress_egress_rules --> by default is 50 rules
max_security_group_rules: 50
# aws.ec2_instance_older_than_specific_days --> by default is 6 months (180 days)
max_ec2_instance_age_in_days: 180
# aws.ec2_securitygroup_allow_ingress_from_internet_to_any_port
# allowed network interface types for security groups open to the Internet
ec2_allowed_interface_types:
[
"api_gateway_managed",
"vpc_endpoint",
]
# allowed network interface owners for security groups open to the Internet
ec2_allowed_instance_owners:
[
"amazon-elb"
]
# aws.ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports
ec2_high_risk_ports:
[
25,
110,
135,
143,
445,
3000,
4333,
5000,
5500,
8080,
8088,
]
# AWS ECS Configuration
# aws.ecs_service_fargate_latest_platform_version
fargate_linux_latest_version: "1.4.0"
fargate_windows_latest_version: "1.0.0"
# AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries)
# AWS SSM Configuration (aws.ssm_documents_set_as_public)
# Single account environment: No action required. The AWS account number will be automatically added by the checks.
# Multi account environment: Any additional trusted account number should be added as a space separated list, e.g.
# trusted_account_ids : ["123456789012", "098765432109", "678901234567"]
trusted_account_ids: []
# AWS Cloudwatch Configuration
# aws.cloudwatch_log_group_retention_policy_specific_days_enabled --> by default is 365 days
log_group_retention_days: 365
# AWS CloudFormation Configuration
# cloudformation_stack_cdktoolkit_bootstrap_version --> by default is 21
recommended_cdk_bootstrap_version: 21
# AWS AppStream Session Configuration
# aws.appstream_fleet_session_idle_disconnect_timeout
max_idle_disconnect_timeout_in_seconds: 600 # 10 Minutes
# aws.appstream_fleet_session_disconnect_timeout
max_disconnect_timeout_in_seconds: 300 # 5 Minutes
# aws.appstream_fleet_maximum_session_duration
max_session_duration_seconds: 36000 # 10 Hours
# AWS Lambda Configuration
# aws.awslambda_function_using_supported_runtimes
obsolete_lambda_runtimes:
[
"java8",
"go1.x",
"provided",
"python3.6",
"python2.7",
"python3.7",
"nodejs4.3",
"nodejs4.3-edge",
"nodejs6.10",
"nodejs",
"nodejs8.10",
"nodejs10.x",
"nodejs12.x",
"nodejs14.x",
"nodejs16.x",
"dotnet5.0",
"dotnet7",
"dotnetcore1.0",
"dotnetcore2.0",
"dotnetcore2.1",
"dotnetcore3.1",
"ruby2.5",
"ruby2.7",
]
# aws.awslambda_function_vpc_is_in_multi_azs
lambda_min_azs: 2
# AWS Organizations
# aws.organizations_scp_check_deny_regions
# aws.organizations_enabled_regions: [
# "eu-central-1",
# "eu-west-1",
# "us-east-1"
# ]
organizations_enabled_regions: []
organizations_trusted_delegated_administrators: []
# AWS ECR
# aws.ecr_repositories_scan_vulnerabilities_in_latest_image
# CRITICAL
# HIGH
# MEDIUM
ecr_repository_vulnerability_minimum_severity: "MEDIUM"
# AWS Trusted Advisor
# aws.trustedadvisor_premium_support_plan_subscribed
verify_premium_support_plans: True
# AWS CloudTrail Configuration
# aws.cloudtrail_threat_detection_privilege_escalation
threat_detection_privilege_escalation_threshold: 0.2 # Percentage of actions found to decide if it is an privilege_escalation attack event, by default is 0.2 (20%)
threat_detection_privilege_escalation_minutes: 1440 # Past minutes to search from now for privilege_escalation attacks, by default is 1440 minutes (24 hours)
threat_detection_privilege_escalation_actions:
[
"AddPermission",
"AddRoleToInstanceProfile",
"AddUserToGroup",
"AssociateAccessPolicy",
"AssumeRole",
"AttachGroupPolicy",
"AttachRolePolicy",
"AttachUserPolicy",
"ChangePassword",
"CreateAccessEntry",
"CreateAccessKey",
"CreateDevEndpoint",
"CreateEventSourceMapping",
"CreateFunction",
"CreateGroup",
"CreateJob",
"CreateKeyPair",
"CreateLoginProfile",
"CreatePipeline",
"CreatePolicyVersion",
"CreateRole",
"CreateStack",
"DeleteRolePermissionsBoundary",
"DeleteRolePolicy",
"DeleteUserPermissionsBoundary",
"DeleteUserPolicy",
"DetachRolePolicy",
"DetachUserPolicy",
"GetCredentialsForIdentity",
"GetId",
"GetPolicyVersion",
"GetUserPolicy",
"Invoke",
"ModifyInstanceAttribute",
"PassRole",
"PutGroupPolicy",
"PutPipelineDefinition",
"PutRolePermissionsBoundary",
"PutRolePolicy",
"PutUserPermissionsBoundary",
"PutUserPolicy",
"ReplaceIamInstanceProfileAssociation",
"RunInstances",
"SetDefaultPolicyVersion",
"UpdateAccessKey",
"UpdateAssumeRolePolicy",
"UpdateDevEndpoint",
"UpdateEventSourceMapping",
"UpdateFunctionCode",
"UpdateJob",
"UpdateLoginProfile",
]
# aws.cloudtrail_threat_detection_enumeration
threat_detection_enumeration_threshold: 0.3 # Percentage of actions found to decide if it is an enumeration attack event, by default is 0.3 (30%)
threat_detection_enumeration_minutes: 1440 # Past minutes to search from now for enumeration attacks, by default is 1440 minutes (24 hours)
threat_detection_enumeration_actions:
[
"DescribeAccessEntry",
"DescribeAccountAttributes",
"DescribeAvailabilityZones",
"DescribeBundleTasks",
"DescribeCarrierGateways",
"DescribeClientVpnRoutes",
"DescribeCluster",
"DescribeDhcpOptions",
"DescribeFlowLogs",
"DescribeImages",
"DescribeInstanceAttribute",
"DescribeInstanceInformation",
"DescribeInstanceTypes",
"DescribeInstances",
"DescribeInstances",
"DescribeKeyPairs",
"DescribeLogGroups",
"DescribeLogStreams",
"DescribeOrganization",
"DescribeRegions",
"DescribeSecurityGroups",
"DescribeSnapshotAttribute",
"DescribeSnapshotTierStatus",
"DescribeSubscriptionFilters",
"DescribeTransitGatewayMulticastDomains",
"DescribeVolumes",
"DescribeVolumesModifications",
"DescribeVpcEndpointConnectionNotifications",
"DescribeVpcs",
"GetAccount",
"GetAccountAuthorizationDetails",
"GetAccountSendingEnabled",
"GetBucketAcl",
"GetBucketLogging",
"GetBucketPolicy",
"GetBucketReplication",
"GetBucketVersioning",
"GetCallerIdentity",
"GetCertificate",
"GetConsoleScreenshot",
"GetCostAndUsage",
"GetDetector",
"GetEbsDefaultKmsKeyId",
"GetEbsEncryptionByDefault",
"GetFindings",
"GetFlowLogsIntegrationTemplate",
"GetIdentityVerificationAttributes",
"GetInstances",
"GetIntrospectionSchema",
"GetLaunchTemplateData",
"GetLaunchTemplateData",
"GetLogRecord",
"GetParameters",
"GetPolicyVersion",
"GetPublicAccessBlock",
"GetQueryResults",
"GetRegions",
"GetSMSAttributes",
"GetSMSSandboxAccountStatus",
"GetSendQuota",
"GetTransitGatewayRouteTableAssociations",
"GetUserPolicy",
"HeadObject",
"ListAccessKeys",
"ListAccounts",
"ListAllMyBuckets",
"ListAssociatedAccessPolicies",
"ListAttachedUserPolicies",
"ListClusters",
"ListDetectors",
"ListDomains",
"ListFindings",
"ListHostedZones",
"ListIPSets",
"ListIdentities",
"ListInstanceProfiles",
"ListObjects",
"ListOrganizationalUnitsForParent",
"ListOriginationNumbers",
"ListPolicyVersions",
"ListRoles",
"ListRoles",
"ListRules",
"ListServiceQuotas",
"ListSubscriptions",
"ListTargetsByRule",
"ListTopics",
"ListUsers",
"LookupEvents",
"Search",
]
# aws.cloudtrail_threat_detection_llm_jacking
threat_detection_llm_jacking_threshold: 0.4 # Percentage of actions found to decide if it is an LLM Jacking attack event, by default is 0.4 (40%)
threat_detection_llm_jacking_minutes: 1440 # Past minutes to search from now for LLM Jacking attacks, by default is 1440 minutes (24 hours)
threat_detection_llm_jacking_actions:
[
"PutUseCaseForModelAccess", # Submits a use case for model access, providing justification (Write).
"PutFoundationModelEntitlement", # Grants entitlement for accessing a foundation model (Write).
"PutModelInvocationLoggingConfiguration", # Configures logging for model invocations (Write).
"CreateFoundationModelAgreement", # Creates a new agreement to use a foundation model (Write).
"InvokeModel", # Invokes a specified Bedrock model for inference using provided prompt and parameters (Read).
"InvokeModelWithResponseStream", # Invokes a Bedrock model for inference with real-time token streaming (Read).
"GetUseCaseForModelAccess", # Retrieves an existing use case for model access (Read).
"GetModelInvocationLoggingConfiguration", # Fetches the logging configuration for model invocations (Read).
"GetFoundationModelAvailability", # Checks the availability of a foundation model for use (Read).
"ListFoundationModelAgreementOffers", # Lists available agreement offers for accessing foundation models (List).
"ListFoundationModels", # Lists the available foundation models in Bedrock (List).
"ListProvisionedModelThroughputs", # Lists the provisioned throughput for previously created models (List).
]
# AWS RDS Configuration
# aws.rds_instance_backup_enabled
# Whether to check RDS instance replicas or not
check_rds_instance_replicas: False
# AWS ACM Configuration
# aws.acm_certificates_expiration_check
days_to_expire_threshold: 7
# aws.acm_certificates_with_secure_key_algorithms
insecure_key_algorithms:
[
"RSA-1024",
"P-192",
"SHA-1",
]
# AWS EKS Configuration
# aws.eks_control_plane_logging_all_types_enabled
# EKS control plane logging types that must be enabled
eks_required_log_types:
[
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler",
]
# aws.eks_cluster_uses_a_supported_version
# EKS clusters must be version 1.28 or higher
eks_cluster_oldest_version_supported: "1.28"
# AWS CodeBuild Configuration
# aws.codebuild_project_no_secrets_in_variables
# CodeBuild sensitive variables that are excluded from the check
excluded_sensitive_environment_variables:
[
]
# AWS ELB Configuration
# aws.elb_is_in_multiple_az
# Minimum number of Availability Zones that an CLB must be in
elb_min_azs: 2
# AWS ELBv2 Configuration
# aws.elbv2_is_in_multiple_az
# Minimum number of Availability Zones that an ELBv2 must be in
elbv2_min_azs: 2
# AWS Secrets Configuration
# Patterns to ignore in the secrets checks
secrets_ignore_patterns: []
# AWS Secrets Manager Configuration
# aws.secretsmanager_secret_unused
# Maximum number of days a secret can be unused
max_days_secret_unused: 90
# aws.secretsmanager_secret_rotated_periodically
# Maximum number of days a secret should be rotated
max_days_secret_unrotated: 90
# AWS Kinesis Configuration
# Minimum retention period in hours for Kinesis streams
min_kinesis_stream_retention_hours: 168 # 7 days
# Azure Configuration
azure:
# Azure Network Configuration
# azure.network_public_ip_shodan
# TODO: create common config
shodan_api_key: null
# Azure App Service
# azure.app_ensure_php_version_is_latest
php_latest_version: "8.2"
# azure.app_ensure_python_version_is_latest
python_latest_version: "3.12"
# azure.app_ensure_java_version_is_latest
java_latest_version: "17"
# Azure SQL Server
# azure.sqlserver_minimal_tls_version
recommended_minimal_tls_versions:
[
"1.2",
"1.3",
]
# GCP Configuration
gcp:
# GCP Compute Configuration
# gcp.compute_public_address_shodan
shodan_api_key: null
# Kubernetes Configuration
kubernetes:
# Kubernetes API Server
# kubernetes.apiserver_audit_log_maxbackup_set
audit_log_maxbackup: 10
# kubernetes.apiserver_audit_log_maxsize_set
audit_log_maxsize: 100
# kubernetes.apiserver_audit_log_maxage_set
audit_log_maxage: 30
# kubernetes.apiserver_strong_ciphers_only
apiserver_strong_ciphers:
[
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_CHACHA20_POLY1305_SHA256",
]
# Kubelet
# kubernetes.kubelet_strong_ciphers_only
kubelet_strong_ciphers:
[
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
]
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: []
# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""
#This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# This is for setting Kubernetes Annotations to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# This is for setting Kubernetes Labels to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
service:
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
type: ClusterIP
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
port: 80
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
#This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -39,4 +39,3 @@ spec:
path: {{ $value }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

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