diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 4b404586a1..e8cceb2a11 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler API** are documented in this file. +## [Unreleased] + +### 🚀 Added + +- Image provider support with registry credential handling, UID validation, and connection testing + +--- + ## [1.19.0] (Prowler v5.18.0) ### 🚀 Added diff --git a/api/poetry.lock b/api/poetry.lock index 8ab49cf6bf..63e4db6bde 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "about-time" @@ -7,6 +7,7 @@ description = "Easily measure timing and throughput of code blocks, with beautif optional = false python-versions = ">=3.7, <4" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece"}, {file = "about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341"}, @@ -19,6 +20,7 @@ description = "Note: This library is already replaced by MSAL Python, available optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "adal-1.2.7-py2.py3-none-any.whl", hash = "sha256:2a7451ed7441ddbc57703042204a3e30ef747478eea022c70f789fc7f084bc3d"}, {file = "adal-1.2.7.tar.gz", hash = "sha256:d74f45b81317454d96e982fd1c50e6fb5c99ac2223728aea8764433a39f566f1"}, @@ -37,6 +39,7 @@ description = "Async boto3 wrapper" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6"}, {file = "aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979"}, @@ -57,6 +60,7 @@ description = "Async client for aws services using botocore and aiohttp" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f"}, {file = "aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc"}, @@ -84,6 +88,7 @@ description = "File support for asyncio." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, @@ -96,6 +101,7 @@ description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, @@ -108,6 +114,7 @@ description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, @@ -250,6 +257,7 @@ description = "itertools and builtins for AsyncIO and mixed iterables" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be"}, {file = "aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c"}, @@ -262,6 +270,7 @@ description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, @@ -278,6 +287,7 @@ description = "Alibaba Cloud ActionTrail (20200706) SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_actiontrail20200706-2.4.1-py3-none-any.whl", hash = "sha256:5dee0009db9b7cba182fbac742820f6a949287a8faafb843b5107f7dc89136da"}, {file = "alibabacloud_actiontrail20200706-2.4.1.tar.gz", hash = "sha256:b65c6b37a96443fbe625dd5a4dd1be52a7476006a411db75206908b11588ffa8"}, @@ -296,6 +306,7 @@ description = "The alibabacloud credentials module of alibabaCloud Python SDK." optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud-credentials-1.0.3.tar.gz", hash = "sha256:9d8707e96afc6f348e23f5677ed15a21c2dfce7cfe6669776548ee4c80e1dfaf"}, {file = "alibabacloud_credentials-1.0.3-py3-none-any.whl", hash = "sha256:30c8302f204b663c655d97e1c283ee9f9f84a6257d7901b931477d6cf34445a8"}, @@ -314,6 +325,7 @@ description = "Alibaba Cloud Gateway SPI SDK Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f"}, ] @@ -325,6 +337,7 @@ description = "Alibaba Cloud CS (20151215) SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_cs20151215-6.1.0-py3-none-any.whl", hash = "sha256:75e90b1bb9acca2236244bb0e44234ca4805d456ea4303ba4225ac15152a458e"}, {file = "alibabacloud_cs20151215-6.1.0.tar.gz", hash = "sha256:5b3d99306701bf499ddd57cd9f2905b7721cb1bb4bb38ffe4d051f7b4e80e355"}, @@ -343,6 +356,7 @@ description = "Alibaba Cloud Darabonba Array SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_darabonba_array-0.1.0.tar.gz", hash = "sha256:7f9a7c632518ff4f0cebb0d4e825a48c12e7cf0b9016ea25054dd73732e155aa"}, ] @@ -354,6 +368,7 @@ description = "Darabonba Util Library for Alibaba Cloud Python SDK" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_darabonba_encode_util-0.0.2.tar.gz", hash = "sha256:f1c484f276d60450fa49b4b2987194e741fcb2f7faae7f287c0ae65abc85fd4d"}, ] @@ -365,6 +380,7 @@ description = "Alibaba Cloud Darabonba Map SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_darabonba_map-0.0.1.tar.gz", hash = "sha256:adb17384658a1a8f72418f1838d4b6a5fd2566bfd392a3ef06d9dbb0a595a23f"}, ] @@ -376,6 +392,7 @@ description = "Darabonba Util Library for Alibaba Cloud Python SDK" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_darabonba_signature_util-0.0.4.tar.gz", hash = "sha256:71d79b2ae65957bcfbf699ced894fda782b32f9635f1616635533e5a90d5feb0"}, ] @@ -390,6 +407,7 @@ description = "Alibaba Cloud Darabonba String Library for Python" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud-darabonba-string-0.0.4.tar.gz", hash = "sha256:ec6614c0448dadcbc5e466485838a1f8cfdd911135bea739e20b14511270c6f7"}, ] @@ -401,6 +419,7 @@ description = "Alibaba Cloud Darabonba Time SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_darabonba_time-0.0.1.tar.gz", hash = "sha256:0ad9c7b0696570d1a3f40106cc7777f755fd92baa0d1dcab5b7df78dde5b922d"}, ] @@ -412,6 +431,7 @@ description = "Alibaba Cloud Elastic Compute Service (20140526) SDK Library for optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_ecs20140526-7.2.5-py3-none-any.whl", hash = "sha256:10bda5e185f6ba899e7d51477373595c629d66db7530a8a37433fb4e9034a96f"}, {file = "alibabacloud_ecs20140526-7.2.5.tar.gz", hash = "sha256:2abbe630ce42d69061821f38950b938c5982cc31902ccd7132d05be328765a55"}, @@ -430,6 +450,7 @@ description = "The endpoint-util module of alibabaCloud Python SDK." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_endpoint_util-0.0.4.tar.gz", hash = "sha256:a593eb8ddd8168d5dc2216cd33111b144f9189fcd6e9ca20e48f358a739bbf90"}, ] @@ -441,6 +462,7 @@ description = "Alibaba Cloud OSS SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_gateway_oss-0.0.17.tar.gz", hash = "sha256:8c4b66c8c7dd285fc210ee232ab3f062b5573258752804d19382000746531e29"}, ] @@ -467,6 +489,7 @@ description = "Alibaba Cloud OSS Util Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_gateway_oss_util-0.0.3.tar.gz", hash = "sha256:5eb7fa450dc7350d5c71577974b9d7f489479e5c5ec7efc1c5376385e8c1c0a5"}, ] @@ -478,6 +501,7 @@ description = "Alibaba Cloud SLS Gateway Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_gateway_sls-0.4.0-py3-none-any.whl", hash = "sha256:a0299a83a5528025983b42b7533a28028461bced5e180a66f97999e0134760a6"}, {file = "alibabacloud_gateway_sls-0.4.0.tar.gz", hash = "sha256:9d2aceb377c9b3ed0558149fda16fe39fa114cc0a22e22a88dc76efdda34633b"}, @@ -502,6 +526,7 @@ description = "Alibaba Cloud SLS Util Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_gateway_sls_util-0.4.0-py3-none-any.whl", hash = "sha256:c91ab7fe55af526a01d25b0d431088c4d241b160db055da3d8cb7330bd74595a"}, {file = "alibabacloud_gateway_sls_util-0.4.0.tar.gz", hash = "sha256:f8b683a36a2ae3fe9a8225d3d97773ea769bdf9cdf4f4d033eab2eb6062ddd1f"}, @@ -519,6 +544,7 @@ description = "Alibaba Cloud Gateway SPI SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_gateway_spi-0.0.3.tar.gz", hash = "sha256:10d1c53a3fc5f87915fbd6b4985b98338a776e9b44a0263f56643c5048223b8b"}, ] @@ -533,6 +559,7 @@ description = "Aliyun Tea OpenApi Library for Python" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_openapi_util-0.2.4-py3-none-any.whl", hash = "sha256:a2474f230b5965ae9a8c286e0dc86132a887928d02d20b8182656cf6b1b6c5bd"}, {file = "alibabacloud_openapi_util-0.2.4.tar.gz", hash = "sha256:87022b9dcb7593a601f7a40ca698227ac3ccb776b58cb7b06b8dc7f510995c34"}, @@ -549,6 +576,7 @@ description = "The oss util module of alibabaCloud Python SDK." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_oss_util-0.0.6.tar.gz", hash = "sha256:d3ecec36632434bd509a113e8cf327dc23e830ac8d9dd6949926f4e334c8b5d6"}, ] @@ -563,6 +591,7 @@ description = "Alibaba Cloud Object Storage Service (20190517) SDK Library for P optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_oss20190517-1.0.6-py3-none-any.whl", hash = "sha256:365fda353de6658a1a289f4d70dcd0394e2a8e2921b6b5834ba6d9772121d2f6"}, {file = "alibabacloud_oss20190517-1.0.6.tar.gz", hash = "sha256:7cd0fb16af613ceb38d2e0e529aa1f58038c7cf59eb67c8c8775ae44ea717852"}, @@ -582,6 +611,7 @@ description = "Alibaba Cloud Resource Access Management (20150501) SDK Library f optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_ram20150501-1.2.0-py3-none-any.whl", hash = "sha256:03a0f2a0259848787c1f74e802b486184a88e04183486bd9398766971e5eb00a"}, {file = "alibabacloud_ram20150501-1.2.0.tar.gz", hash = "sha256:6253513c8880769f4fd5b36fedddb362a9ca628ad9ae9c05c0eeacf5fbc95b42"}, @@ -600,6 +630,7 @@ description = "Alibaba Cloud rds (20140815) SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_rds20140815-12.0.0-py3-none-any.whl", hash = "sha256:0bd7e2018a428d86b1b0681087336e74665b48fc3eb0a13c4f4377ed5eab2b08"}, {file = "alibabacloud_rds20140815-12.0.0.tar.gz", hash = "sha256:e7421d94f18a914c0a06b0e7fad0daff557713f1c97d415d463a78c1270e9b98"}, @@ -618,6 +649,7 @@ description = "Alibaba Cloud Threat Detection (20181203) SDK Library for Python optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_sas20181203-6.1.0-py3-none-any.whl", hash = "sha256:1ad735332c50c7961be036b17420d56b5ec3b5557e3aea1daa19491e8b75da20"}, {file = "alibabacloud_sas20181203-6.1.0.tar.gz", hash = "sha256:e49ffd53e630274a8bf5a8299ca753023ad118510c80f6d9c6fb018b7479bf37"}, @@ -636,6 +668,7 @@ description = "Alibaba Cloud Log Service (20201230) SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_sls20201230-5.9.0-py3-none-any.whl", hash = "sha256:c4ae14096817a9686af5a0ae2389f1f6a8781e60b9edb8643445250cf15c26f1"}, {file = "alibabacloud_sls20201230-5.9.0.tar.gz", hash = "sha256:bea830b64fbc7ed1719ba386ceeefb120f08d705f03eb0e02409dc6f12a291da"}, @@ -654,6 +687,7 @@ description = "Alibaba Cloud Sts (20150401) SDK Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_sts20150401-1.1.6-py3-none-any.whl", hash = "sha256:627f5ca1f86e19b0bf8ce0e99071a36fb65579fad9256fbee38fdc8d500598e9"}, {file = "alibabacloud_sts20150401-1.1.6.tar.gz", hash = "sha256:c2529b41e0e4531e21cb393e4df346e19fd6d54cc6337d1138dbcd2191438d4c"}, @@ -672,6 +706,7 @@ description = "The tea module of alibabaCloud Python SDK." optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud-tea-0.4.3.tar.gz", hash = "sha256:ec8053d0aa8d43ebe1deb632d5c5404339b39ec9a18a0707d57765838418504a"}, ] @@ -687,6 +722,7 @@ description = "Alibaba Cloud openapi SDK Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_tea_openapi-0.4.1-py3-none-any.whl", hash = "sha256:e46bfa3ca34086d2c357d217a0b7284ecbd4b3bab5c88e075e73aec637b0e4a0"}, {file = "alibabacloud_tea_openapi-0.4.1.tar.gz", hash = "sha256:2384b090870fdb089c3c40f3fb8cf0145b8c7d6c14abbac521f86a01abb5edaf"}, @@ -706,6 +742,7 @@ description = "The tea-util module of alibabaCloud Python SDK." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_tea_util-0.3.14-py3-none-any.whl", hash = "sha256:10d3e5c340d8f7ec69dd27345eb2fc5a1dab07875742525edf07bbe86db93bfe"}, {file = "alibabacloud_tea_util-0.3.14.tar.gz", hash = "sha256:708e7c9f64641a3c9e0e566365d2f23675f8d7c2a3e2971d9402ceede0408cdb"}, @@ -721,6 +758,7 @@ description = "The tea-xml module of alibabaCloud Python SDK." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_tea_xml-0.0.3.tar.gz", hash = "sha256:979cb51fadf43de77f41c69fc69c12529728919f849723eb0cd24eb7b048a90c"}, ] @@ -735,6 +773,7 @@ description = "Alibaba Cloud Virtual Private Cloud (20160428) SDK Library for Py optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alibabacloud_vpc20160428-6.13.0-py3-none-any.whl", hash = "sha256:933cf1e74322a20a2df27ca6323760d857744a4246eeadc9fb3eae01322fb1c6"}, {file = "alibabacloud_vpc20160428-6.13.0.tar.gz", hash = "sha256:daf00679a83d422799f9fcf263739fe1f360641675843cbfbe623833fc8b1681"}, @@ -753,6 +792,7 @@ description = "A new kind of Progress Bar, with real-time throughput, ETA, and v optional = false python-versions = "<4,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "alive-progress-3.3.0.tar.gz", hash = "sha256:457dd2428b48dacd49854022a46448d236a48f1b7277874071c39395307e830c"}, {file = "alive_progress-3.3.0-py3-none-any.whl", hash = "sha256:63dd33bb94cde15ad9e5b666dbba8fedf71b72a4935d6fb9a92931e69402c9ff"}, @@ -769,6 +809,7 @@ description = "Fast protobuf serialization for Aliyun Log using PyO3 and quick-p optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:51633d92d2b349aed4843c0b503454fb4f7d73eeaaa54f82aa5a36c10c064ef5"}, {file = "aliyun_log_fastpb-0.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d2984aafc61ccbbf1db2589ce90b6d5a26e72dba137fb1fdf7f61ce3faa967c0"}, @@ -795,6 +836,7 @@ description = "Low-level AMQP client for Python (fork of amqplib)." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, @@ -810,6 +852,7 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -822,6 +865,7 @@ description = "High-level concurrency and networking framework on top of asyncio optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, @@ -841,6 +885,7 @@ description = "This project extends the Application Insights API surface to supp optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "applicationinsights-0.11.10-py2.py3-none-any.whl", hash = "sha256:e89a890db1c6906b6a7d0bcfd617dac83974773c64573147c8d6654f9cf2a6ea"}, {file = "applicationinsights-0.11.10.tar.gz", hash = "sha256:0b761f3ef0680acf4731906dfc1807faa6f2a57168ae74592db0084a6099f7b3"}, @@ -853,6 +898,7 @@ description = "In-process task scheduler with Cron-like capabilities" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d"}, {file = "apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41"}, @@ -881,6 +927,7 @@ description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, @@ -896,6 +943,7 @@ description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, @@ -911,6 +959,7 @@ description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, @@ -923,7 +972,7 @@ description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version == \"3.11\" and python_full_version < \"3.11.3\"" +markers = "python_full_version < \"3.11.3\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\") and python_version == \"3.11\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -936,6 +985,7 @@ description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, @@ -948,6 +998,7 @@ description = "The ultimate Python library in building OAuth and OpenID Connect optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, @@ -963,6 +1014,7 @@ description = "A tool that automatically formats Python code to conform to the P optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"}, {file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"}, @@ -978,6 +1030,7 @@ description = "Work with the AWS IP address ranges in native Python." optional = false python-versions = ">=3.7,<4.0" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "awsipranges-0.3.3-py3-none-any.whl", hash = "sha256:f3d7a54aeaf7fe310beb5d377a4034a63a51b72677ae6af3e0967bc4de7eedaf"}, {file = "awsipranges-0.3.3.tar.gz", hash = "sha256:4f0b3f22a9dc1163c85b513bed812b6c92bdacd674e6a7b68252a3c25b99e2c0"}, @@ -990,6 +1043,7 @@ description = "Microsoft Azure Command-Line Tools Core Module" optional = false python-versions = ">=3.10.0" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_cli_core-2.82.0-py3-none-any.whl", hash = "sha256:998792de4e4d44f7f048ef46c5a07c8b30cff291e9b141682fd8a2c01421c826"}, {file = "azure_cli_core-2.82.0.tar.gz", hash = "sha256:d2de9423d19373665a4cdaae8db3139bcdcbb6cf10bfd417ef4610cb7733f1cd"}, @@ -1026,6 +1080,7 @@ description = "Microsoft Azure CLI Telemetry Package" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-cli-telemetry-1.1.0.tar.gz", hash = "sha256:d922379cda1b48952be75fb3bd2ac5e7ceecf569492a6088bab77894c624a278"}, {file = "azure_cli_telemetry-1.1.0-py3-none-any.whl", hash = "sha256:2fc12608c0cf0ea6e69b392af9cab92f1249340b8caff7e9674cf91b3becb337"}, @@ -1042,6 +1097,7 @@ description = "Microsoft Azure Client Library for Python (Common)" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"}, {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"}, @@ -1054,6 +1110,7 @@ description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_core-1.37.0-py3-none-any.whl", hash = "sha256:b3abe2c59e7d6bb18b38c275a5029ff80f98990e7c90a5e646249a56630fcc19"}, {file = "azure_core-1.37.0.tar.gz", hash = "sha256:7064f2c11e4b97f340e8e8c6d923b822978be3016e46b7bc4aa4b337cfb48aee"}, @@ -1074,6 +1131,7 @@ description = "Microsoft Azure Identity Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9"}, {file = "azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6"}, @@ -1093,6 +1151,7 @@ description = "Microsoft Corporation Key Vault Certificates Client Library for P optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_keyvault_certificates-4.10.0-py3-none-any.whl", hash = "sha256:fa76cbc329274cb5f4ab61b0ed7d209d44377df4b4d6be2fd01e741c2fbb83a9"}, {file = "azure_keyvault_certificates-4.10.0.tar.gz", hash = "sha256:004ff47a73152f9f40f678e5a07719b753a3ca86f0460bfeaaf6a23304872e05"}, @@ -1110,6 +1169,7 @@ description = "Microsoft Azure Key Vault Keys Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_keyvault_keys-4.10.0-py3-none-any.whl", hash = "sha256:210227e0061f641a79755f0e0bcbcf27bbfb4df630a933c43a99a29962283d0d"}, {file = "azure_keyvault_keys-4.10.0.tar.gz", hash = "sha256:511206ae90aec1726a4d6ff5a92d754bd0c0f1e8751891368d30fb70b62955f1"}, @@ -1128,6 +1188,7 @@ description = "Microsoft Corporation Key Vault Secrets Client Library for Python optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9"}, {file = "azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36"}, @@ -1145,6 +1206,7 @@ description = "Microsoft Azure API Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_apimanagement-5.0.0-py3-none-any.whl", hash = "sha256:b88c42a392333b60722fb86f15d092dfc19a8d67510dccd15c217381dff4e6ec"}, {file = "azure_mgmt_apimanagement-5.0.0.tar.gz", hash = "sha256:0ab7fe17e70fe3154cd840ff47d19d7a4610217003eaa7c21acf3511a6e57999"}, @@ -1163,6 +1225,7 @@ description = "Microsoft Azure Application Insights Management Client Library fo optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_applicationinsights-4.1.0-py3-none-any.whl", hash = "sha256:9e71f29b01e505a773501451d12fd6a10482cf4b13e9ac2bff72f5380496d979"}, {file = "azure_mgmt_applicationinsights-4.1.0.tar.gz", hash = "sha256:15531390f12ce3d767cd3f1949af36aa39077c145c952fec4d80303c86ec7b6c"}, @@ -1181,6 +1244,7 @@ description = "Microsoft Azure Authorization Management Client Library for Pytho optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-authorization-4.0.0.zip", hash = "sha256:69b85abc09ae64fc72975bd43431170d8c7eb5d166754b98aac5f3845de57dc4"}, {file = "azure_mgmt_authorization-4.0.0-py3-none-any.whl", hash = "sha256:d8feeb3842e6ddf1a370963ca4f61fb6edc124e8997b807dd025bc9b2379cd1a"}, @@ -1198,6 +1262,7 @@ description = "Microsoft Azure Compute Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_compute-34.0.0-py3-none-any.whl", hash = "sha256:f8f7b1c5c187a26fae4d1f099adf93561244242f28899484d9a42747bf0d5af4"}, {file = "azure_mgmt_compute-34.0.0.tar.gz", hash = "sha256:58cd01d025efa02870b84dbfb69834a3b23501a135658c03854d2434e8dfee1e"}, @@ -1216,6 +1281,7 @@ description = "Microsoft Azure Container Instance Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-containerinstance-10.1.0.zip", hash = "sha256:78d437adb28574f448c838ed5f01f9ced378196098061deb59d9f7031704c17e"}, {file = "azure_mgmt_containerinstance-10.1.0-py3-none-any.whl", hash = "sha256:ee7977b7b70f2233e44ec6ce8c99027f3f7892bb3452b4bad46df340d9f98959"}, @@ -1233,6 +1299,7 @@ description = "Microsoft Azure Container Registry Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_containerregistry-12.0.0-py3-none-any.whl", hash = "sha256:464abd4d3d9ecc0456ed8f63a6b9b93afc2e3e194f2d34f26a758afb67ad3b5c"}, {file = "azure_mgmt_containerregistry-12.0.0.tar.gz", hash = "sha256:f19f8faa7881deaf2b5015c0eb050a92e2380cd9d18dee33cdb5f27d44a06c03"}, @@ -1251,6 +1318,7 @@ description = "Microsoft Azure Container Service Management Client Library for P optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_containerservice-34.1.0-py3-none-any.whl", hash = "sha256:1faa1714e0100c6ee4cfb8d2eadb1c270b548a84b0070c74e9fe646056a5cb12"}, {file = "azure_mgmt_containerservice-34.1.0.tar.gz", hash = "sha256:637a6cf8f06636c016ad151d76f9c7ba75bd05d4334b3dd7837eb8b517f30dbe"}, @@ -1269,6 +1337,7 @@ description = "Microsoft Azure Management Core Library for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_core-1.6.0-py3-none-any.whl", hash = "sha256:0460d11e85c408b71c727ee1981f74432bc641bb25dfcf1bb4e90a49e776dbc4"}, {file = "azure_mgmt_core-1.6.0.tar.gz", hash = "sha256:b26232af857b021e61d813d9f4ae530465255cb10b3dde945ad3743f7a58e79c"}, @@ -1284,6 +1353,7 @@ description = "Microsoft Azure Cosmos DB Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_cosmosdb-9.7.0-py3-none-any.whl", hash = "sha256:be735a554d16995c8cefe413e62119985f8fabae1cb45a6f6ad2c3958bed14da"}, {file = "azure_mgmt_cosmosdb-9.7.0.tar.gz", hash = "sha256:b5072d319f11953d8f12e22459aded1912d5f27e442e1d8b49596a85005410a1"}, @@ -1302,6 +1372,7 @@ description = "Microsoft Azure Data Bricks Management Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-databricks-2.0.0.zip", hash = "sha256:70d11362dc2d17f5fb1db0cfe65c1af55b8f136f1a0db9a5b51e7acf760cf5b9"}, {file = "azure_mgmt_databricks-2.0.0-py3-none-any.whl", hash = "sha256:0c29434a7339e74231bd171a6c08dcdf8153abaebd332658d7f66b8ea143fa17"}, @@ -1319,6 +1390,7 @@ description = "Microsoft Azure Data Factory Management Client Library for Python optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_datafactory-9.2.0-py3-none-any.whl", hash = "sha256:d870a7a6099227e91d1c258a956c2aa32c2ea4c0a4409913d8f215887349f128"}, {file = "azure_mgmt_datafactory-9.2.0.tar.gz", hash = "sha256:5132e9c24c441ac225f2a60225924baa55079ca81eff7db99a70d661d64bb0d7"}, @@ -1337,6 +1409,7 @@ description = "Microsoft Azure Event Grid Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_eventgrid-10.4.0-py3-none-any.whl", hash = "sha256:5e4637245bbff33298d5f427971b870dbb03d873a3ef68f328190a7b7a38c56f"}, {file = "azure_mgmt_eventgrid-10.4.0.tar.gz", hash = "sha256:303e5e27cf4bb5ec833ba4e5a9ef70b5bc410e190412ec47cde59d82e413fb7e"}, @@ -1355,6 +1428,7 @@ description = "Microsoft Azure Event Hub Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_eventhub-11.2.0-py3-none-any.whl", hash = "sha256:a7e2618eca58d8e52c7ff7d4a04a4fae12685351746e6d01b933b43e7ea3b906"}, {file = "azure_mgmt_eventhub-11.2.0.tar.gz", hash = "sha256:31c47f18f73d2d83345cde5909568e28858c2548a35b10e23194b4767a9ce7e3"}, @@ -1373,6 +1447,7 @@ description = "Microsoft Azure Key Vault Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-keyvault-10.3.1.tar.gz", hash = "sha256:34b92956aefbdd571cae5a03f7078e037d8087b2c00cfa6748835dc73abb5a30"}, {file = "azure_mgmt_keyvault-10.3.1-py3-none-any.whl", hash = "sha256:a18a27a06551482d31f92bc43ac8b0846af02cd69511f80090865b4c5caa3c21"}, @@ -1391,6 +1466,7 @@ description = "Microsoft Azure Log Analytics Management Client Library for Pytho optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-loganalytics-12.0.0.zip", hash = "sha256:da128a7e0291be7fa2063848df92a9180cf5c16d42adc09d2bc2efd711536bfb"}, {file = "azure_mgmt_loganalytics-12.0.0-py2.py3-none-any.whl", hash = "sha256:75ac1d47dd81179905c40765be8834643d8994acff31056ddc1863017f3faa02"}, @@ -1408,6 +1484,7 @@ description = "Microsoft Azure Logic Apps Management Client Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-logic-10.0.0.zip", hash = "sha256:b3fa4864f14aaa7af41d778d925f051ed29b6016f46344765ecd0f49d0f04dd6"}, {file = "azure_mgmt_logic-10.0.0-py3-none-any.whl", hash = "sha256:525c78afedf3edb35eb0a16152c8beba89769ee1bc6af01bcdc42842a551e443"}, @@ -1425,6 +1502,7 @@ description = "Microsoft Azure Monitor Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-monitor-6.0.2.tar.gz", hash = "sha256:5ffbf500e499ab7912b1ba6d26cef26480d9ae411532019bb78d72562196e07b"}, {file = "azure_mgmt_monitor-6.0.2-py3-none-any.whl", hash = "sha256:fe4cf41e6680b74a228f81451dc5522656d599c6f343ecf702fc790fda9a357b"}, @@ -1442,6 +1520,7 @@ description = "Microsoft Azure Network Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_network-28.1.0-py3-none-any.whl", hash = "sha256:8ddb0e9ec8f10c9c152d60fc945908d113e4591f397ea3e40b92290ec2b01658"}, {file = "azure_mgmt_network-28.1.0.tar.gz", hash = "sha256:8c84bffb5ec75c6e0244e58ecf07c00d5fc421d616b0cb369c6fe585af33cf87"}, @@ -1460,6 +1539,7 @@ description = "Microsoft Azure Postgresqlflexibleservers Management Client Libra optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_postgresqlflexibleservers-1.1.0-py3-none-any.whl", hash = "sha256:87ddb5a5e6d12c45769485d234cfe0322140e3a0a7636d0e61fb00ac544b5d20"}, {file = "azure_mgmt_postgresqlflexibleservers-1.1.0.tar.gz", hash = "sha256:9ede9d8ba63e9d2879cb74adc903c649af3bc5460a02787287b0cd18d754af14"}, @@ -1478,6 +1558,7 @@ description = "Microsoft Azure RDBMS Management Client Library for Python" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-rdbms-10.1.0.zip", hash = "sha256:a87d401c876c84734cdd4888af551e4a1461b4b328d9816af60cb8ac5979f035"}, {file = "azure_mgmt_rdbms-10.1.0-py3-none-any.whl", hash = "sha256:8eac17d1341a91d7ed914435941ba917b5ef1568acabc3e65653603966a7cc88"}, @@ -1495,6 +1576,7 @@ description = "Microsoft Azure Recovery Services Client Library for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66"}, {file = "azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687"}, @@ -1513,6 +1595,7 @@ description = "Microsoft Azure Recovery Services Backup Management Client Librar optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1"}, {file = "azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29"}, @@ -1531,6 +1614,7 @@ description = "Microsoft Azure Resource Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_resource-23.3.0-py3-none-any.whl", hash = "sha256:ab216ee28e29db6654b989746e0c85a1181f66653929d2cb6e48fba66d9af323"}, {file = "azure_mgmt_resource-23.3.0.tar.gz", hash = "sha256:fc4f1fd8b6aad23f8af4ed1f913df5f5c92df117449dc354fea6802a2829fea4"}, @@ -1549,6 +1633,7 @@ description = "Microsoft Azure Search Management Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-search-9.1.0.tar.gz", hash = "sha256:53bc6eeadb0974d21f120bb21bb5e6827df6d650e17347460fd83e2d68883599"}, {file = "azure_mgmt_search-9.1.0-py3-none-any.whl", hash = "sha256:488ff81477e980e2b7abf0b857387c74ebbad419e6f6126044e3e6fad2da72b6"}, @@ -1566,6 +1651,7 @@ description = "Microsoft Azure Security Center Management Client Library for Pyt optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-security-7.0.0.tar.gz", hash = "sha256:5912eed7e9d3758fdca8d26e1dc26b41943dc4703208a1184266e2c252e1ad66"}, {file = "azure_mgmt_security-7.0.0-py3-none-any.whl", hash = "sha256:85a6d8b7a5cd74884a548ed53fed034449f54a9989edd64e9020c5837db96933"}, @@ -1583,6 +1669,7 @@ description = "Microsoft Azure SQL Management Client Library for Python" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-sql-3.0.1.zip", hash = "sha256:129042cc011225e27aee6ef2697d585fa5722e5d1aeb0038af6ad2451a285457"}, {file = "azure_mgmt_sql-3.0.1-py2.py3-none-any.whl", hash = "sha256:1d1dd940d4d41be4ee319aad626341251572a5bf4a2addec71779432d9a1381f"}, @@ -1600,6 +1687,7 @@ description = "Microsoft Azure Storage Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_storage-22.1.1-py3-none-any.whl", hash = "sha256:a4a4064918dcfa4f1cbebada5bf064935d66f2a3647a2f46a1f1c9348736f5d9"}, {file = "azure_mgmt_storage-22.1.1.tar.gz", hash = "sha256:25aaa5ae8c40c30e2f91f8aae6f52906b0557e947d5c1b9817d4ff9decc11340"}, @@ -1618,6 +1706,7 @@ description = "Microsoft Azure Subscription Management Client Library for Python optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-subscription-3.1.1.zip", hash = "sha256:4e255b4ce9b924357bb8c5009b3c88a2014d3203b2495e2256fa027bf84e800e"}, {file = "azure_mgmt_subscription-3.1.1-py3-none-any.whl", hash = "sha256:38d4574a8d47fa17e3587d756e296cb63b82ad8fb21cd8543bcee443a502bf48"}, @@ -1635,6 +1724,7 @@ description = "Microsoft Azure Synapse Management Client Library for Python" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure-mgmt-synapse-2.0.0.zip", hash = "sha256:bec6bdfaeb55b4fdd159f2055e8875bf50a720bb0fce80a816e92a2359b898c8"}, {file = "azure_mgmt_synapse-2.0.0-py2.py3-none-any.whl", hash = "sha256:e901274009be843a7bf2eedeab32c0941fabb2addea9a1ad1560395073965f0f"}, @@ -1652,6 +1742,7 @@ description = "Microsoft Azure Web Apps Management Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_mgmt_web-8.0.0-py3-none-any.whl", hash = "sha256:0536aac05bfc673b56ed930f2966b77856e84df675d376e782a7af6bb92449af"}, {file = "azure_mgmt_web-8.0.0.tar.gz", hash = "sha256:c8d9c042c09db7aacb20270a9effed4d4e651e365af32d80897b84dc7bf35098"}, @@ -1670,6 +1761,7 @@ description = "Microsoft Corporation Azure Monitor Query Client Library for Pyth optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_monitor_query-2.0.0-py3-none-any.whl", hash = "sha256:8f52d581271d785e12f49cd5aaa144b8910fb843db2373855a7ef94c7fc462ea"}, {file = "azure_monitor_query-2.0.0.tar.gz", hash = "sha256:7b05f2fcac4fb67fc9f77a7d4c5d98a0f3099fb73b57c69ec1b080773994671b"}, @@ -1687,6 +1779,7 @@ description = "Microsoft Azure Blob Storage Client Library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_storage_blob-12.24.1-py3-none-any.whl", hash = "sha256:77fb823fdbac7f3c11f7d86a5892e2f85e161e8440a7489babe2195bf248f09e"}, {file = "azure_storage_blob-12.24.1.tar.gz", hash = "sha256:052b2a1ea41725ba12e2f4f17be85a54df1129e13ea0321f5a2fcc851cbf47d4"}, @@ -1708,6 +1801,7 @@ description = "Microsoft Azure Synapse Artifacts Client Library for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "azure_synapse_artifacts-0.21.0-py3-none-any.whl", hash = "sha256:3311919df13a2b42f1fb9debf5d512080c35d64d02b9f84ff944848835289a8d"}, {file = "azure_synapse_artifacts-0.21.0.tar.gz", hash = "sha256:d7e37516cf8569e03c604d921e3407d7140cf7523b67b67f757caf999e3c8ee7"}, @@ -1726,6 +1820,7 @@ description = "Function decoration for backoff and retry" optional = false python-versions = ">=3.7,<4.0" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, @@ -1738,6 +1833,7 @@ description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, @@ -1763,6 +1859,7 @@ description = "Python multiprocessing fork with improvements and bugfixes" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5"}, {file = "billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f"}, @@ -1775,6 +1872,7 @@ description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -1787,6 +1885,7 @@ description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c"}, {file = "boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12"}, @@ -1807,6 +1906,7 @@ description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7"}, {file = "botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd"}, @@ -1827,6 +1927,7 @@ description = "Explore assets and their relationships across your technical infr optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [] develop = false @@ -1903,6 +2004,7 @@ description = "Distributed Task Queue." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5"}, {file = "celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b"}, @@ -1961,6 +2063,7 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, @@ -1973,7 +2076,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\")" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -2071,6 +2174,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, @@ -2194,6 +2298,7 @@ description = "Python Circuit Breaker pattern implementation" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1"}, {file = "circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084"}, @@ -2206,6 +2311,7 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, @@ -2221,6 +2327,7 @@ description = "Enables git-like *did-you-mean* feature in click" optional = false python-versions = ">=3.6.2" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, @@ -2236,6 +2343,7 @@ description = "An extension module for click to enable registering CLI commands optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6"}, {file = "click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261"}, @@ -2254,6 +2362,7 @@ description = "REPL plugin for Click" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, @@ -2273,6 +2382,7 @@ description = "The official Python library for the cloudflare API" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "cloudflare-4.3.1-py3-none-any.whl", hash = "sha256:6927135a5ee5633d6e2e1952ca0484745e933727aeeb189996d2ad9d292071c6"}, {file = "cloudflare-4.3.1.tar.gz", hash = "sha256:b1e1c6beeb8d98f63bfe0a1cba874fc4e22e000bcc490544f956c689b3b5b258"}, @@ -2297,7 +2407,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} +markers = {main = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"", dev = "(sys_platform == \"win32\" or platform_system == \"Windows\") and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\")"} [[package]] name = "contextlib2" @@ -2306,6 +2416,7 @@ description = "Backports and enhancements for the contextlib module" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"}, {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"}, @@ -2318,6 +2429,7 @@ description = "Python library for calculating contours of 2D quadrilateral grids optional = false python-versions = ">=3.11" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, @@ -2410,6 +2522,7 @@ description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, @@ -2475,6 +2588,7 @@ description = "A Python library that converts cron expressions into human readab optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "cron_descriptor-2.0.6-py3-none-any.whl", hash = "sha256:3a1c0d837c0e5a32e415f821b36cf758eb92d510e6beff8fbfe4fa16573d93d6"}, {file = "cron_descriptor-2.0.6.tar.gz", hash = "sha256:e39d2848e1d8913cfb6e3452e701b5eec662ee18bea8cc5aa53ee1a7bb217157"}, @@ -2494,6 +2608,7 @@ description = "The CrowdStrike Falcon SDK for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "crowdstrike_falconpy-1.6.0-py3-none-any.whl", hash = "sha256:2cae39e42510f473e77f3a647d56aa8f55d9de0a1963bad353b195cb5ae61ea4"}, {file = "crowdstrike_falconpy-1.6.0.tar.gz", hash = "sha256:663402ac9bc56625478460b4865446371de2d74f5e96cb5d16672119c113346f"}, @@ -2508,43 +2623,50 @@ dev = ["bandit", "coverage", "flake8", "pydocstyle", "pylint", "pytest", "pytest [[package]] name = "cryptography" -version = "44.0.1" +version = "44.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ - {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, - {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, - {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, - {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, - {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, - {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, - {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, - {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, - {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, - {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, - {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, - {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, - {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, - {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, + {file = "cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01"}, + {file = "cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904"}, + {file = "cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44"}, + {file = "cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d"}, + {file = "cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d"}, + {file = "cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c"}, + {file = "cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5"}, + {file = "cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b"}, + {file = "cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028"}, + {file = "cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06"}, + {file = "cryptography-44.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5"}, + {file = "cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c"}, + {file = "cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"}, ] [package.dependencies] @@ -2557,7 +2679,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -2567,6 +2689,7 @@ description = "Composable style cycles" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -2583,6 +2706,7 @@ description = "The darabonba module of alibabaCloud Python SDK." optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "darabonba_core-1.0.5-py3-none-any.whl", hash = "sha256:671ab8dbc4edc2a8f88013da71646839bb8914f1259efc069353243ef52ea27c"}, ] @@ -2599,6 +2723,7 @@ description = "A Python framework for building reactive web-apps. Developed by P optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dash-3.1.1-py3-none-any.whl", hash = "sha256:66fff37e79c6aa114cd55aea13683d1e9afe0e3f96b35388baca95ff6cfdad23"}, {file = "dash-3.1.1.tar.gz", hash = "sha256:916b31cec46da0a3339da0e9df9f446126aa7f293c0544e07adf9fe4ba060b18"}, @@ -2631,6 +2756,7 @@ description = "Bootstrap themed components for use in Plotly Dash" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5"}, {file = "dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de"}, @@ -2649,6 +2775,7 @@ description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64"}, {file = "debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642"}, @@ -2689,6 +2816,7 @@ description = "Decorators for Humans" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, @@ -2701,6 +2829,7 @@ description = "Python @deprecated decorator to deprecate old python classes, fun optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f"}, {file = "deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223"}, @@ -2719,6 +2848,7 @@ description = "Tool for detecting secrets in the codebase" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"}, {file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"}, @@ -2739,6 +2869,7 @@ description = "serialize all of Python" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, @@ -2755,6 +2886,7 @@ description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -2767,6 +2899,7 @@ description = "Authentication and Registration in Django Rest Framework" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90"}, ] @@ -2786,6 +2919,7 @@ description = "A high-level Python web framework that encourages rapid developme optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, @@ -2807,6 +2941,7 @@ description = "Integrated set of Django applications addressing authentication, optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_allauth-65.14.0-py3-none-any.whl", hash = "sha256:448f5f7877f95fcbe1657256510fe7822d7871f202521a29e23ef937f3325a97"}, {file = "django_allauth-65.14.0.tar.gz", hash = "sha256:5529227aba2b1377d900e9274a3f24496c645e65400fbae3cad5789944bc4d0b"}, @@ -2837,6 +2972,7 @@ description = "Database-backed Periodic Tasks." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_celery_beat-2.8.1-py3-none-any.whl", hash = "sha256:da2b1c6939495c05a551717509d6e3b79444e114a027f7b77bf3727c2a39d171"}, {file = "django_celery_beat-2.8.1.tar.gz", hash = "sha256:dfad0201c0ac50c91a34700ef8fa0a10ee098cc7f3375fe5debed79f2204f80a"}, @@ -2857,6 +2993,7 @@ description = "Celery result backends for Django." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_celery_results-2.6.0-py3-none-any.whl", hash = "sha256:b9ccdca2695b98c7cbbb8dea742311ba9a92773d71d7b4944a676e69a7df1c73"}, {file = "django_celery_results-2.6.0.tar.gz", hash = "sha256:9abcd836ae6b61063779244d8887a88fe80bbfaba143df36d3cb07034671277c"}, @@ -2873,6 +3010,7 @@ description = "django-cors-headers is a Django application for handling the serv optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -2889,6 +3027,7 @@ description = "A package that allows you to utilize 12factor inspired environmen optional = false python-versions = ">=3.6,<4" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, @@ -2906,6 +3045,7 @@ description = "Django-filter is a reusable Django application for allowing users optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, @@ -2921,6 +3061,7 @@ description = "Middleware that enables single request-response cycle tracing by optional = false python-versions = "<4.0,>=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_guid-3.5.0-py3-none-any.whl", hash = "sha256:28f52cfeac47e8e22ea889a3845bc2b1c604dd842e495dadd44ad5184db72c76"}, {file = "django_guid-3.5.0.tar.gz", hash = "sha256:5f32f70287e4f36addc79f29f2a7b2f56fc5f4e4cfb2023141525be8baa35d9e"}, @@ -2936,6 +3077,7 @@ description = "Bringing all of PostgreSQL's awesomeness to Django." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_postgres_extra-2.0.9-py3-none-any.whl", hash = "sha256:0a4f9fd7f843e2ef5cfe7a291fcb883bd72d2ca8e4b369d0070866205e00b404"}, {file = "django_postgres_extra-2.0.9.tar.gz", hash = "sha256:9e47c436d033712d0e2611b8c1583566dd4f97700e5360001bf3623913a92046"}, @@ -2958,6 +3100,7 @@ description = "Silky smooth profiling for the Django Framework" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_silk-5.3.2-py3-none-any.whl", hash = "sha256:49f1caebfda28b1707f0cfef524e0476beb82b8c5e40f5ccff7f73a6b4f6d3ac"}, {file = "django_silk-5.3.2.tar.gz", hash = "sha256:b0db54eebedb8d16f572321bd6daccac0bd3f547ae2618bb45d96fe8fc02229d"}, @@ -2976,6 +3119,7 @@ description = "A Django app providing DB, form, and REST framework fields for zo optional = false python-versions = "<4.0,>=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "django_timezone_field-7.2.1-py3-none-any.whl", hash = "sha256:276915b72c5816f57c3baf9e43f816c695ef940d1b21f91ebf6203c09bf4ad44"}, {file = "django_timezone_field-7.2.1.tar.gz", hash = "sha256:def846f9e7200b7b8f2a28fcce2b78fb2d470f6a9f272b07c4e014f6ba4c6d2e"}, @@ -2991,6 +3135,7 @@ description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -3006,6 +3151,7 @@ description = "A Django REST framework API adapter for the JSON:API spec." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "djangorestframework-jsonapi-7.0.2.tar.gz", hash = "sha256:d6c72a2bee539f1093dd86620e862af2d1a0e60408e38a710146286dbde71d75"}, {file = "djangorestframework_jsonapi-7.0.2-py2.py3-none-any.whl", hash = "sha256:be457adb50aac77eec8893048bf46ad6926dcd26204aa10965a1430610828d50"}, @@ -3028,6 +3174,7 @@ description = "A minimal JSON Web Token authentication plugin for Django REST Fr optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469"}, {file = "djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f"}, @@ -3053,6 +3200,7 @@ description = "DNS toolkit" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, @@ -3074,6 +3222,7 @@ description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, @@ -3097,6 +3246,7 @@ description = "A caching front-end based on the Dogpile lock." optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dogpile_cache-1.5.0-py3-none-any.whl", hash = "sha256:dc7b47d37844db15e8fdc0243c1b58857a2ddc52a5118237a97127bac200e18d"}, {file = "dogpile_cache-1.5.0.tar.gz", hash = "sha256:849c5573c9a38f155cd4173103c702b637ede0361c12e864876877d0cd125eec"}, @@ -3122,6 +3272,7 @@ description = "A parser for Python dependency files" optional = false python-versions = ">=3.7" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57"}, {file = "dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a"}, @@ -3143,6 +3294,7 @@ description = "Extensions for Django REST Framework" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "drf_extensions-0.8.0-py2.py3-none-any.whl", hash = "sha256:ab7bd854c9061c27ab55233b66d758001e5c2d81aaa9d117cbbe1c9ea49c77ab"}, {file = "drf_extensions-0.8.0.tar.gz", hash = "sha256:c3f27bca74a2def53e8454a5c7b327595195df51e121743120b2f51ef5a52aaa"}, @@ -3160,6 +3312,7 @@ description = "Nested resources for the Django Rest Framework" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "drf_nested_routers-0.95.0-py2.py3-none-any.whl", hash = "sha256:dd489c33d667aaa81383ffaa8c74781d2b353d8f0795716ae37fc59ee297b7c4"}, {file = "drf_nested_routers-0.95.0.tar.gz", hash = "sha256:815978f802e578fd7035c74040c104909cbe97615de89a275d77e928f4029891"}, @@ -3176,6 +3329,7 @@ description = "API Key authentication and permissions for Django REST." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "drf_simple_apikey-2.2.1-py2.py3-none-any.whl", hash = "sha256:2a60b35676d14f907c47dee179dd0fa7425a84c34d6ff5b48d08d3b87ff32809"}, {file = "drf_simple_apikey-2.2.1.tar.gz", hash = "sha256:e5a52804bbac12c8db80c10a3d51a8514fc59fc8385b5e751099a2bc944ad25d"}, @@ -3197,6 +3351,7 @@ description = "Sane and flexible OpenAPI 3 schema generation for Django REST fra optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "drf-spectacular-0.27.2.tar.gz", hash = "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981"}, {file = "drf_spectacular-0.27.2-py3-none-any.whl", hash = "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b"}, @@ -3221,6 +3376,7 @@ description = "open api 3 schema generator for drf-json-api package based on drf optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "drf-spectacular-jsonapi-0.5.1.tar.gz", hash = "sha256:e45f87f3cce2692f4f546e0785d8fcc32c6b49770fff858065c267ae8f9cfde5"}, {file = "drf_spectacular_jsonapi-0.5.1-py3-none-any.whl", hash = "sha256:abac728abd83e2544408cc900d682d532ca2088f2f9321d1c9101bcdfdabca78"}, @@ -3240,6 +3396,7 @@ description = "Python Git Library" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "dulwich-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c13b0d5a9009cde23ecb8cb201df6e23e2a7a82c5e2d6ba6443fbb322c9befc6"}, {file = "dulwich-0.23.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a68faf8612bf93de1285048d6ad13160f0fb3c5596a86e694e78f4e212886fa5"}, @@ -3288,6 +3445,7 @@ description = "Reference client for Duo Security APIs" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "duo_client-5.5.0-py3-none-any.whl", hash = "sha256:4fbf1e97a2b25ef64e9f88171ab817162cf45bafc1c63026af4883baf8892a12"}, {file = "duo_client-5.5.0.tar.gz", hash = "sha256:303109e047fe7525ba4fc4a294c1f3deb4125066e89c10d33f7430378867b1d6"}, @@ -3303,6 +3461,7 @@ description = "Module for converting between datetime.timedelta and Go's Duratio optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, @@ -3315,6 +3474,7 @@ description = "A robust email address syntax and deliverability validation libra optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, @@ -3331,6 +3491,7 @@ description = "execnet: rapid multi-Python deployment" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, @@ -3346,6 +3507,7 @@ description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"}, {file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"}, @@ -3358,6 +3520,7 @@ description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, @@ -3382,6 +3545,7 @@ description = "Tools to manipulate font files" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24"}, {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958"}, @@ -3455,6 +3619,7 @@ description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -3470,6 +3635,7 @@ description = "A list-like structure which implements collections.abc.MutableSeq optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, @@ -3610,6 +3776,7 @@ description = "Coroutine-based network library" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"}, {file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"}, @@ -3676,6 +3843,7 @@ description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9"}, {file = "google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7"}, @@ -3703,6 +3871,7 @@ description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_api_python_client-2.163.0-py2.py3-none-any.whl", hash = "sha256:080e8bc0669cb4c1fb8efb8da2f5b91a2625d8f0e7796cfad978f33f7016c6c4"}, {file = "google_api_python_client-2.163.0.tar.gz", hash = "sha256:88dee87553a2d82176e2224648bf89272d536c8f04dcdda37ef0a71473886dd7"}, @@ -3722,6 +3891,7 @@ description = "Google Authentication Library" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f"}, {file = "google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce"}, @@ -3750,6 +3920,7 @@ description = "Google Authentication Library: httplib2 transport" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_auth_httplib2-0.2.1-py3-none-any.whl", hash = "sha256:1be94c611db91c01f9703e7f62b0a59bbd5587a95571c7b6fade510d648bc08b"}, {file = "google_auth_httplib2-0.2.1.tar.gz", hash = "sha256:5ef03be3927423c87fb69607b42df23a444e434ddb2555b73b3679793187b7de"}, @@ -3766,6 +3937,7 @@ description = "Google Cloud Access Context Manager Protobufs" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_cloud_access_context_manager-0.3.0-py3-none-any.whl", hash = "sha256:5d15ad51547f06c281e35f16b4ffcb3e98bb2d898b01470f88b94edfb2eeb0a3"}, {file = "google_cloud_access_context_manager-0.3.0.tar.gz", hash = "sha256:f3aa35c9225b7aaef85ecdacedcc1577789be8d458b7a41b6ad23b504786e5f9"}, @@ -3782,6 +3954,7 @@ description = "Google Cloud Asset API client library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_cloud_asset-4.2.0-py3-none-any.whl", hash = "sha256:fd7ea04c64948a4779790343204cd5b41d4772d6ab1d05a9125e28a637ac0862"}, {file = "google_cloud_asset-4.2.0.tar.gz", hash = "sha256:1734906cfd9b6ea6922861c8f1b4fcabe90d53ca267ee88499e8532b7593b35f"}, @@ -3805,6 +3978,7 @@ description = "Google Cloud Org Policy API client library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_cloud_org_policy-1.16.0-py3-none-any.whl", hash = "sha256:96d1ed38f795182600a58f8eb2879e1577ce663b6b27df0b8a3050960cff87a5"}, {file = "google_cloud_org_policy-1.16.0.tar.gz", hash = "sha256:c72147127d88d9809af8738b2abe34806eac529c3cdc57aa915cc08a1b842a13"}, @@ -3824,6 +3998,7 @@ description = "Google Cloud Os Config API client library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_cloud_os_config-1.23.0-py3-none-any.whl", hash = "sha256:fea865018391abca42a9a74d270ddab516ae0865d6e9ad3bcb503286ca01c069"}, {file = "google_cloud_os_config-1.23.0.tar.gz", hash = "sha256:a629cf55b3ede36b2df89814c6ccf3c1d43c7f1b43db6c7c02eb4860851baf3a"}, @@ -3843,6 +4018,7 @@ description = "Google Cloud Resource Manager API client library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "google_cloud_resource_manager-1.16.0-py3-none-any.whl", hash = "sha256:fb9a2ad2b5053c508e1c407ac31abfd1a22e91c32876c1892830724195819a28"}, {file = "google_cloud_resource_manager-1.16.0.tar.gz", hash = "sha256:cc938f87cc36c2672f062b1e541650629e0d954c405a4dac35ceedee70c267c3"}, @@ -3863,6 +4039,7 @@ description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, @@ -3882,6 +4059,7 @@ description = "Generate a dot graph from the output of several profilers." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "gprof2dot-2025.4.14-py3-none-any.whl", hash = "sha256:0742e4c0b4409a5e8777e739388a11e1ed3750be86895655312ea7c20bd0090e"}, {file = "gprof2dot-2025.4.14.tar.gz", hash = "sha256:35743e2d2ca027bf48fa7cba37021aaf4a27beeae1ae8e05a50b55f1f921a6ce"}, @@ -3894,6 +4072,7 @@ description = "Unicode grapheme helpers" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "graphemeu-0.7.2-py3-none-any.whl", hash = "sha256:1444520f6899fd30114fc2a39f297d86d10fa0f23bf7579f772f8bc7efaa2542"}, {file = "graphemeu-0.7.2.tar.gz", hash = "sha256:42bbe373d7c146160f286cd5f76b1a8ad29172d7333ce10705c5cc282462a4f8"}, @@ -3910,7 +4089,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "platform_python_implementation == \"CPython\"" +markers = "platform_python_implementation == \"CPython\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\")" files = [ {file = "greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13"}, {file = "greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4"}, @@ -3978,6 +4157,7 @@ description = "IAM API client library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6"}, {file = "grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389"}, @@ -3995,6 +4175,7 @@ description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc"}, {file = "grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde"}, @@ -4072,6 +4253,7 @@ description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18"}, {file = "grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd"}, @@ -4089,6 +4271,7 @@ description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -4111,6 +4294,7 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -4123,6 +4307,7 @@ description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, @@ -4139,6 +4324,7 @@ description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -4151,6 +4337,7 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -4173,6 +4360,7 @@ description = "A comprehensive HTTP client library." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349"}, {file = "httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24"}, @@ -4188,6 +4376,7 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -4214,6 +4403,7 @@ description = "Human friendly output for text interfaces using Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, @@ -4229,6 +4419,7 @@ description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -4241,6 +4432,7 @@ description = "IAM data for AWS actions, resources, and conditions based on IAM optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "iamdata-0.1.202602021-py3-none-any.whl", hash = "sha256:48419662d75dd0e1ea22b9cc98fd70201d4c72760c6897acc46ad9ab90633d18"}, {file = "iamdata-0.1.202602021.tar.gz", hash = "sha256:c24265fc3694076f65da91a8aa9361b60da25f7b8cfd8ba4ddd6aa1b9bb5153e"}, @@ -4253,6 +4445,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, @@ -4268,6 +4461,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, @@ -4292,6 +4486,7 @@ description = "A port of Ruby on Rails inflector to Python" optional = false python-versions = ">=3.5" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, @@ -4304,6 +4499,7 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -4316,6 +4512,7 @@ description = "Simple module to parse ISO 8601 dates" optional = false python-versions = ">=3.7,<4.0" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, @@ -4328,6 +4525,7 @@ description = "An ISO 8601 date/time/duration parser and formatter" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, @@ -4340,6 +4538,7 @@ description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -4355,6 +4554,7 @@ description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -4367,6 +4567,7 @@ description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -4385,6 +4586,7 @@ description = "Fast iterable JSON parser." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e"}, {file = "jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a"}, @@ -4497,6 +4699,7 @@ description = "JSON Matching Expressions" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"}, {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, @@ -4509,6 +4712,7 @@ description = "Lightweight pipelining with Python functions" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713"}, {file = "joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3"}, @@ -4521,6 +4725,7 @@ description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -4536,6 +4741,7 @@ description = "jsonpickle encodes/decodes any Python object to/from JSON" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91"}, {file = "jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1"}, @@ -4555,6 +4761,7 @@ description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, @@ -4567,6 +4774,7 @@ description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -4589,6 +4797,7 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, @@ -4604,6 +4813,7 @@ description = "Authentication Library for OpenStack Identity" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "keystoneauth1-5.13.0-py3-none-any.whl", hash = "sha256:5ab81412eb0923ceb9c602cc3decce514b399523cb83d16b409ed3b0f9b03d41"}, {file = "keystoneauth1-5.13.0.tar.gz", hash = "sha256:57c9ca407207899b50d8ff1ca8abb4a4e7427461bfc1877eb8519c3989ce63ec"}, @@ -4630,6 +4840,7 @@ description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, @@ -4741,6 +4952,7 @@ description = "A Command-Line Interface framework" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "knack-0.11.0-py3-none-any.whl", hash = "sha256:6704c867840978a119a193914a90e2e98c7be7dff764c8fcd8a2286c5a978d00"}, {file = "knack-0.11.0.tar.gz", hash = "sha256:eb6568001e9110b1b320941431c51033d104cc98cda2254a5c2b09ba569fd494"}, @@ -4761,6 +4973,7 @@ description = "Messaging library for Python." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93"}, {file = "kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55"}, @@ -4797,6 +5010,7 @@ description = "Kubernetes python client" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "kubernetes-32.0.1-py2.py3-none-any.whl", hash = "sha256:35282ab8493b938b08ab5526c7ce66588232df00ef5e1dbe88a419107dc10998"}, {file = "kubernetes-32.0.1.tar.gz", hash = "sha256:42f43d49abd437ada79a79a16bd48a604d3471a117a8347e87db693f2ba0ba28"}, @@ -4825,6 +5039,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395"}, {file = "lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666"}, @@ -4980,6 +5195,7 @@ description = "LZ4 Bindings for Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"}, {file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"}, @@ -5052,6 +5268,7 @@ description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, @@ -5068,6 +5285,7 @@ description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, @@ -5092,6 +5310,7 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, @@ -5191,6 +5410,7 @@ description = "A lightweight library for converting complex datatypes to and fro optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73"}, {file = "marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57"}, @@ -5211,6 +5431,7 @@ description = "Python plotting package" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7"}, {file = "matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656"}, @@ -5290,6 +5511,7 @@ description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -5302,6 +5524,7 @@ description = "Markdown URL utilities" optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -5314,6 +5537,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_abstractions-1.9.2-py3-none-any.whl", hash = "sha256:a8853d272a84da59d6a2fe11a76c28e9c55bdab268a345ba48e918cb6822b607"}, {file = "microsoft_kiota_abstractions-1.9.2.tar.gz", hash = "sha256:29cdafe8d0672f23099556e0b120dca6231c752cca9393e1e0092fa9ca594572"}, @@ -5331,6 +5555,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_authentication_azure-1.9.2-py3-none-any.whl", hash = "sha256:56840f8b15df8aedfd143fb2deb7cc7fae4ac0bafb1a50546b7313a7b3ab4ca0"}, {file = "microsoft_kiota_authentication_azure-1.9.2.tar.gz", hash = "sha256:171045f522a93d9340fbddc4cabb218f14f1d9d289e82e535b3d9291986c3d5a"}, @@ -5350,6 +5575,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_http-1.9.2-py3-none-any.whl", hash = "sha256:3a2d930a70d0184d9f4848473f929ee892462cae1acfaf33b2d193f1828c76c2"}, {file = "microsoft_kiota_http-1.9.2.tar.gz", hash = "sha256:2ba3d04a3d1d5d600736eebc1e33533d54d87799ac4fbb92c9cce4a97809af61"}, @@ -5368,6 +5594,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_serialization_form-1.9.2-py3-none-any.whl", hash = "sha256:7b997efb2c8750b1d4fbc00878ba2a3e6e1df3fcefc8815226c90fcc9c54f218"}, {file = "microsoft_kiota_serialization_form-1.9.2.tar.gz", hash = "sha256:badfbe65d8ec3369bd58b01022d13ef590edf14babeef94188efe3f4ec24fe41"}, @@ -5383,6 +5610,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_serialization_json-1.9.2-py3-none-any.whl", hash = "sha256:8f4ecf485607fff3df5ce8fa9b9c957bc7f4bff1658b183703e180af753098e3"}, {file = "microsoft_kiota_serialization_json-1.9.2.tar.gz", hash = "sha256:19f7beb69c67b2cb77ca96f77824ee78a693929e20237bb5476ea54f69118bf1"}, @@ -5398,6 +5626,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_serialization_multipart-1.9.2-py3-none-any.whl", hash = "sha256:641ad374046f1c7adff90d110bdc68d77418adb1e479a716f4ffea3647f0ead6"}, {file = "microsoft_kiota_serialization_multipart-1.9.2.tar.gz", hash = "sha256:b1851409205668d83f5c7a35a8b6fca974b341985b4a92841e95aaec93b7ca0a"}, @@ -5413,6 +5642,7 @@ description = "Core abstractions for kiota generated libraries in Python" optional = false python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_kiota_serialization_text-1.9.2-py3-none-any.whl", hash = "sha256:6e63129ea29eb9b976f4ed56fc6595d204e29fc309958b639299e9f9f4e5edb4"}, {file = "microsoft_kiota_serialization_text-1.9.2.tar.gz", hash = "sha256:4289508ebac0cefdc4fa21c545051769a9409913972355ccda9116b647f978f2"}, @@ -5428,6 +5658,7 @@ description = "A tool for detecting and masking secrets" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "microsoft_security_utilities_secret_masker-1.0.0b4-py3-none-any.whl", hash = "sha256:0429fcaad10fc8ae3f940ab84fd2926e4f50ede134162144123b35937be831a8"}, {file = "microsoft_security_utilities_secret_masker-1.0.0b4.tar.gz", hash = "sha256:a30bd361ac18c8b52f6844076bc26465335949ea9c7a004d95f5196ec6fdef3e"}, @@ -5440,6 +5671,7 @@ description = "The Microsoft Authentication Library (MSAL) for Python library en optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msal-1.34.0b1-py3-none-any.whl", hash = "sha256:3b6373325e3509d97873e36965a75e9cc9393f1b579d12cc03c0ca0ef6d37eb4"}, {file = "msal-1.34.0b1.tar.gz", hash = "sha256:86cdbfec14955e803379499d017056c6df4ed40f717fd6addde94bdeb4babd78"}, @@ -5465,6 +5697,7 @@ description = "Microsoft Authentication Library extensions (MSAL EX) provides a optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, @@ -5481,6 +5714,7 @@ description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msgraph_core-1.3.8-py3-none-any.whl", hash = "sha256:86d83edcf62119946f201d13b7e857c947ef67addb088883940197081de85bea"}, {file = "msgraph_core-1.3.8.tar.gz", hash = "sha256:6e883f9d4c4ad57501234749e07b010478c1a5f19550ef4cf005bbcac4a63ae7"}, @@ -5502,6 +5736,7 @@ description = "The Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msgraph_sdk-1.23.0-py3-none-any.whl", hash = "sha256:58e0047b4ca59fd82022c02cd73fec0170a3d84f3b76721e3db2a0314df9a58a"}, {file = "msgraph_sdk-1.23.0.tar.gz", hash = "sha256:6dd1ba9a46f5f0ce8599fd9610133adbd9d1493941438b5d3632fce9e55ed607"}, @@ -5525,6 +5760,7 @@ description = "AutoRest swagger generator Python client runtime." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32"}, {file = "msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9"}, @@ -5547,6 +5783,7 @@ description = "AutoRest swagger generator Python client runtime. Azure-specific optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "msrestazure-0.6.4.post1-py2.py3-none-any.whl", hash = "sha256:2264493b086c2a0a82ddf5fd87b35b3fffc443819127fed992ac5028354c151e"}, {file = "msrestazure-0.6.4.post1.tar.gz", hash = "sha256:39842007569e8c77885ace5c46e4bf2a9108fcb09b1e6efdf85b6e2c642b55d4"}, @@ -5564,6 +5801,7 @@ description = "multidict implementation" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, @@ -5720,6 +5958,7 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, @@ -5767,6 +6006,7 @@ description = "Type system extensions for programs checked with the mypy type ch optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -5779,6 +6019,7 @@ description = "Extremely lightweight compatibility layer between dataframe libra optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d"}, {file = "narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145"}, @@ -5805,6 +6046,7 @@ description = "Neo4j Bolt driver for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "neo4j-5.28.3-py3-none-any.whl", hash = "sha256:dbf6d9211b861bc3dd62dccbf8a74d1e33e0c602084dd123b753edf46e1fdfad"}, {file = "neo4j-5.28.3.tar.gz", hash = "sha256:0625aaaf0963bc99a7231e946952f579792c3be22687192b20e0b74aa1233a2b"}, @@ -5825,51 +6067,12 @@ description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] -[[package]] -name = "netifaces" -version = "0.11.0" -description = "Portable network interface information." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1"}, - {file = "netifaces-0.11.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4"}, - {file = "netifaces-0.11.0-cp27-cp27m-win32.whl", hash = "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1"}, - {file = "netifaces-0.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85"}, - {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea"}, - {file = "netifaces-0.11.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89"}, - {file = "netifaces-0.11.0-cp34-cp34m-win32.whl", hash = "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4"}, - {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4"}, - {file = "netifaces-0.11.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b"}, - {file = "netifaces-0.11.0-cp35-cp35m-win32.whl", hash = "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac"}, - {file = "netifaces-0.11.0-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be"}, - {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1"}, - {file = "netifaces-0.11.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0"}, - {file = "netifaces-0.11.0-cp36-cp36m-win32.whl", hash = "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7"}, - {file = "netifaces-0.11.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8"}, - {file = "netifaces-0.11.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246"}, - {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5"}, - {file = "netifaces-0.11.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9"}, - {file = "netifaces-0.11.0-cp37-cp37m-win32.whl", hash = "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150"}, - {file = "netifaces-0.11.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5"}, - {file = "netifaces-0.11.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c"}, - {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3"}, - {file = "netifaces-0.11.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4"}, - {file = "netifaces-0.11.0-cp38-cp38-win32.whl", hash = "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048"}, - {file = "netifaces-0.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05"}, - {file = "netifaces-0.11.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d"}, - {file = "netifaces-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff"}, - {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f"}, - {file = "netifaces-0.11.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1"}, - {file = "netifaces-0.11.0.tar.gz", hash = "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32"}, -] - [[package]] name = "nltk" version = "3.9.2" @@ -5877,6 +6080,7 @@ description = "Natural Language Toolkit" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a"}, {file = "nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419"}, @@ -5903,6 +6107,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, @@ -5958,6 +6163,7 @@ description = "A generic, spec-compliant, thorough implementation of the OAuth r optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -5975,6 +6181,7 @@ description = "Oracle Cloud Infrastructure Python SDK" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "oci-2.160.3-py3-none-any.whl", hash = "sha256:858bff3e697098bdda44833d2476bfb4632126f0182178e7dbde4dbd156d71f0"}, {file = "oci-2.160.3.tar.gz", hash = "sha256:57514889be3b713a8385d86e3ba8a33cf46e3563c2a7e29a93027fb30b8a2537"}, @@ -5998,6 +6205,7 @@ description = "Okta client APIs" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "okta-0.0.4.tar.gz", hash = "sha256:53e792c68d3684ff4140b4cb1c02af3821090368f8110fde54c0bdb638449332"}, ] @@ -6014,6 +6222,7 @@ description = "The official Python library for the openai API" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315"}, {file = "openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869"}, @@ -6037,14 +6246,15 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] [[package]] name = "openstacksdk" -version = "4.0.1" +version = "4.2.0" description = "An SDK for building applications to work with OpenStack" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ - {file = "openstacksdk-4.0.1-py3-none-any.whl", hash = "sha256:d63187a006fff7c1de1486c9e2e1073a787af402620c3c0ed0cf5291225998ac"}, - {file = "openstacksdk-4.0.1.tar.gz", hash = "sha256:19faa1d5e6a78a2c1dc06a171e65e776ba82e9df23e1d08586225dc5ade9fc63"}, + {file = "openstacksdk-4.2.0-py3-none-any.whl", hash = "sha256:238be0fa5d9899872b00787ab38e84f92fd6dc87525fde0965dadcdc12196dc6"}, + {file = "openstacksdk-4.2.0.tar.gz", hash = "sha256:5cb9450dcce8054a2caf89d8be9e55057ddfa219a954e781032241eb29280445"}, ] [package.dependencies] @@ -6055,10 +6265,10 @@ iso8601 = ">=0.1.11" jmespath = ">=0.9.0" jsonpatch = ">=1.16,<1.20 || >1.20" keystoneauth1 = ">=3.18.0" -netifaces = ">=0.10.4" os-service-types = ">=1.7.0" pbr = ">=2.0.0,<2.1.0 || >2.1.0" platformdirs = ">=3" +psutil = ">=3.2.2" PyYAML = ">=3.13" requestsexceptions = ">=1.2.0" @@ -6069,6 +6279,7 @@ description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, @@ -6085,6 +6296,7 @@ description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, @@ -6102,6 +6314,7 @@ description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, @@ -6118,6 +6331,7 @@ description = "Python library for consuming OpenStack sevice-types-authority dat optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "os_service_types-1.8.2-py3-none-any.whl", hash = "sha256:f78890d71814deffabf0ed4358288ec2ced579bc4d0bb87a79ae806cbb4deb6e"}, {file = "os_service_types-1.8.2.tar.gz", hash = "sha256:ab7648d7232849943196e1bb00a30e2e25e600fa3b57bb241d15b7f521b5b575"}, @@ -6134,6 +6348,7 @@ description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, @@ -6146,6 +6361,7 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -6232,6 +6448,7 @@ description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pbr-7.0.3-py2.py3-none-any.whl", hash = "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b"}, {file = "pbr-7.0.3.tar.gz", hash = "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29"}, @@ -6247,6 +6464,7 @@ description = "PagerDuty Python REST API Sessions." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pdpyras-5.4.1-py2.py3-none-any.whl", hash = "sha256:e16020cf57e4c916ab3dace7c7dffe21a2e7059ab7411ce3ddf1e620c54e9c89"}, {file = "pdpyras-5.4.1.tar.gz", hash = "sha256:36021aff5979a79f1d87edc95e0c46e98ce8549292bc0cab3d9f33501795703b"}, @@ -6258,103 +6476,104 @@ urllib3 = "*" [[package]] name = "pillow" -version = "12.1.0" +version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ - {file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"}, - {file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"}, - {file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"}, - {file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"}, - {file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"}, - {file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"}, - {file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"}, - {file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"}, - {file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"}, - {file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"}, - {file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"}, - {file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"}, - {file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"}, - {file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"}, - {file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"}, - {file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"}, - {file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"}, - {file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"}, - {file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"}, - {file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"}, - {file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"}, - {file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"}, - {file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"}, - {file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"}, - {file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"}, - {file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"}, - {file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"}, - {file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"}, - {file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"}, - {file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"}, - {file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"}, - {file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"}, - {file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"}, - {file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"}, - {file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"}, - {file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"}, - {file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"}, - {file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"}, - {file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"}, - {file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"}, - {file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"}, - {file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"}, - {file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"}, - {file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"}, - {file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"}, - {file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"}, - {file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, + {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4"}, + {file = "pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e"}, + {file = "pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff"}, + {file = "pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40"}, + {file = "pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23"}, + {file = "pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32"}, + {file = "pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af"}, + {file = "pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5"}, + {file = "pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d"}, + {file = "pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c"}, + {file = "pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563"}, + {file = "pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052"}, + {file = "pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397"}, + {file = "pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3"}, + {file = "pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35"}, + {file = "pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a"}, + {file = "pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6"}, + {file = "pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9"}, + {file = "pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60"}, + {file = "pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e"}, + {file = "pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a"}, + {file = "pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029"}, + {file = "pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b"}, + {file = "pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1"}, + {file = "pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da"}, + {file = "pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20"}, + {file = "pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf"}, + {file = "pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524"}, + {file = "pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986"}, + {file = "pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c"}, + {file = "pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f"}, + {file = "pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd"}, + {file = "pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f"}, + {file = "pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0"}, + {file = "pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb"}, + {file = "pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f"}, + {file = "pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15"}, + {file = "pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8"}, + {file = "pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f"}, + {file = "pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce"}, + {file = "pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8"}, + {file = "pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36"}, + {file = "pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b"}, + {file = "pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735"}, + {file = "pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e"}, + {file = "pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4"}, ] [package.extras] @@ -6372,6 +6591,7 @@ description = "Query metadata from sdists / bdists / installed packages." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343"}, {file = "pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b"}, @@ -6387,6 +6607,7 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, @@ -6404,6 +6625,7 @@ description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4"}, {file = "plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393"}, @@ -6428,6 +6650,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -6444,6 +6667,7 @@ description = "Parse and Process AWS IAM Policies, Statements, ARNs, and wildcar optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "policyuniverse-1.5.1.20231109-py2.py3-none-any.whl", hash = "sha256:0b0ece0ee8285af31fc39ce09c82a551ca62e62bc2842e23952503bccb973321"}, {file = "policyuniverse-1.5.1.20231109.tar.gz", hash = "sha256:74e56d410560915c2c5132e361b0130e4bffe312a2f45230eac50d7c094bc40a"}, @@ -6460,6 +6684,7 @@ description = "Wraps the portalocker recipe for easy usage" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, @@ -6480,6 +6705,7 @@ description = "Library for building powerful interactive command lines in Python optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"}, {file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"}, @@ -6495,6 +6721,7 @@ description = "Accelerated property cache" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, @@ -6627,6 +6854,7 @@ description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82"}, {file = "proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4"}, @@ -6645,6 +6873,7 @@ description = "" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, @@ -6660,11 +6889,12 @@ files = [ [[package]] name = "prowler" -version = "5.18.0" +version = "5.19.0" description = "Prowler is an Open Source security tool to perform AWS, GCP and Azure security best practices assessments, audits, incident response, continuous monitoring, hardening and forensics readiness. It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, FedRAMP, PCI-DSS, GDPR, HIPAA, FFIEC, SOC2, GXP, AWS Well-Architected Framework Security Pillar, AWS Foundational Technical Review (FTR), ENS (Spanish National Security Scheme) and your custom security frameworks." optional = false python-versions = ">3.9.1,<3.13" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [] develop = false @@ -6715,7 +6945,7 @@ boto3 = "1.40.61" botocore = "1.40.61" cloudflare = "4.3.1" colorama = "0.4.6" -cryptography = "44.0.1" +cryptography = "44.0.3" dash = "3.1.1" dash-bootstrap-components = "2.0.3" detect-secrets = "1.5.0" @@ -6730,10 +6960,10 @@ microsoft-kiota-abstractions = "1.9.2" msgraph-sdk = "1.23.0" numpy = "2.0.2" oci = "2.160.3" -openstacksdk = "4.0.1" +openstacksdk = "4.2.0" pandas = "2.2.3" py-iam-expand = "0.1.0" -py-ocsf-models = "0.5.0" +py-ocsf-models = "0.8.1" pydantic = ">=2.0,<3.0" pygithub = "2.5.0" python-dateutil = ">=2.9.0.post0,<3.0.0" @@ -6747,8 +6977,8 @@ tzlocal = "5.3.1" [package.source] type = "git" url = "https://github.com/prowler-cloud/prowler.git" -reference = "master" -resolved_reference = "b1f99716171856bf787a7695a588ffad6bf8d596" +reference = "feat/PROWLER-940-stage-2-a-image-provider-api-support" +resolved_reference = "348808b37f223cf92099332af9405db418b9f1ab" [[package]] name = "psutil" @@ -6757,6 +6987,7 @@ description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"}, {file = "psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea"}, @@ -6792,6 +7023,7 @@ description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, @@ -6874,6 +7106,7 @@ description = "A simple library to get or create a unique device id for a device optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "py_deviceid-0.1.1-py3-none-any.whl", hash = "sha256:c0e32815e87a08087a0811c18f4402ee88b28a321f997753d75ecdaab570321b"}, {file = "py_deviceid-0.1.1.tar.gz", hash = "sha256:c3e7577ada23666e7f39e69370dfdaa76fe9de79c02635376d6aa0229bfa30e3"}, @@ -6886,6 +7119,7 @@ description = "This is a Python package to expand and deobfuscate IAM policies." optional = false python-versions = "<3.14,>3.9.1" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "py_iam_expand-0.1.0-py3-none-any.whl", hash = "sha256:b845ce7b50ac895b02b4f338e09c62a68ea51849794f76e189b02009bd388510"}, {file = "py_iam_expand-0.1.0.tar.gz", hash = "sha256:5a2884dc267ac59a02c3a80fefc0b34c309dac681baa0f87c436067c6cf53a96"}, @@ -6896,20 +7130,21 @@ iamdata = ">=0.1.202504091" [[package]] name = "py-ocsf-models" -version = "0.5.0" +version = "0.8.1" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false -python-versions = "<3.14,>3.9.1" +python-versions = "<3.15,>3.9.1" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ - {file = "py_ocsf_models-0.5.0-py3-none-any.whl", hash = "sha256:7933253f56782c04c412d976796db429577810b951fe4195351794500b5962d8"}, - {file = "py_ocsf_models-0.5.0.tar.gz", hash = "sha256:bf05e955809d1ec3ab1007e4a4b2a8a0afa74b6e744ea8ffbf386e46b3af0a76"}, + {file = "py_ocsf_models-0.8.1-py3-none-any.whl", hash = "sha256:061eb446c4171534c09a8b37f5a9d2a2fe9f87c5db32edbd1182446bc5fd097e"}, + {file = "py_ocsf_models-0.8.1.tar.gz", hash = "sha256:c9045237857f951e073c9f9d1f57954c90d86875b469260725292d47f7a7d73c"}, ] [package.dependencies] -cryptography = "44.0.1" +cryptography = ">=44.0.3,<47" email-validator = "2.2.0" -pydantic = ">=2.9.2,<3.0.0" +pydantic = ">=2.12.0,<3.0.0" [[package]] name = "pyasn1" @@ -6918,6 +7153,7 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"}, {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"}, @@ -6930,6 +7166,7 @@ description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, @@ -6945,6 +7182,7 @@ description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, @@ -6957,7 +7195,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["main", "dev"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\")" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, @@ -6970,6 +7208,7 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, @@ -6992,6 +7231,7 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, @@ -7126,6 +7366,7 @@ description = "Use the full Github API v3" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "PyGithub-2.5.0-py3-none-any.whl", hash = "sha256:b0b635999a658ab8e08720bdd3318893ff20e2275f6446fcf35bf3f44f2c0fd2"}, {file = "pygithub-2.5.0.tar.gz", hash = "sha256:e1613ac508a9be710920d26eb18b1905ebd9926aa49398e88151c1b526aad3cf"}, @@ -7146,6 +7387,7 @@ description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -7161,6 +7403,7 @@ description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, @@ -7182,6 +7425,7 @@ description = "python code static checker" optional = false python-versions = ">=3.8.0" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, @@ -7210,7 +7454,7 @@ description = "The MSALRuntime Python Interop Package" optional = false python-versions = ">=3.6" groups = ["main"] -markers = "(platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\") and sys_platform == \"win32\"" +markers = "sys_platform == \"win32\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\")" files = [ {file = "pymsalruntime-0.18.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0c22e2e83faa10de422bbfaacc1bb2887c9025ee8a53f0fc2e4f7db01c4a7b66"}, {file = "pymsalruntime-0.18.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8ce2944a0f944833d047bb121396091e00287e2b6373716106da86ea99abf379"}, @@ -7251,6 +7495,7 @@ description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, @@ -7293,6 +7538,7 @@ description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a"}, {file = "pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36"}, @@ -7312,6 +7558,7 @@ description = "pyparsing - Classes and methods to define and execute parsing gra optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, @@ -7327,7 +7574,7 @@ description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" groups = ["main"] -markers = "sys_platform == \"win32\"" +markers = "sys_platform == \"win32\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\")" files = [ {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, @@ -7343,6 +7590,7 @@ description = "A Python SOCKS client module. See https://github.com/Anorov/PySoc optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, @@ -7356,6 +7604,7 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, @@ -7377,6 +7626,7 @@ description = "Pytest plugin for Celery" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest_celery-1.2.1-py3-none-any.whl", hash = "sha256:0441ab0c2a712b775be16ffda3d7deb31995fd7b5e9d71630e7ea98b474346a3"}, {file = "pytest_celery-1.2.1.tar.gz", hash = "sha256:7873fb3cf4fbfe9b0dd15d359bdb8bbab4a41c7e48f5b0adb7d36138d3704d52"}, @@ -7406,6 +7656,7 @@ description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -7425,6 +7676,7 @@ description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, @@ -7444,6 +7696,7 @@ description = "Docker integration tests for pytest" optional = false python-versions = "<4.0.0,>=3.9.0" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest_docker_tools-3.1.9-py2.py3-none-any.whl", hash = "sha256:36f8e88d56d84ea177df68a175673681243dd991d2807fbf551d90f60341bfdb"}, {file = "pytest_docker_tools-3.1.9.tar.gz", hash = "sha256:1b6a0cb633c20145731313335ef15bcf5571839c06726764e60cbe495324782b"}, @@ -7460,6 +7713,7 @@ description = "pytest plugin that allows you to add environment variables." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, @@ -7478,6 +7732,7 @@ description = "Pytest plugin to randomly order tests and control random.seed." optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, @@ -7493,6 +7748,7 @@ description = "pytest xdist plugin for distributed testing, most importantly acr optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, @@ -7514,6 +7770,7 @@ description = "Python Crontab API" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "python_crontab-3.3.0-py3-none-any.whl", hash = "sha256:739a778b1a771379b75654e53fd4df58e5c63a9279a63b5dfe44c0fcc3ee7884"}, {file = "python_crontab-3.3.0.tar.gz", hash = "sha256:007c8aee68dddf3e04ec4dce0fac124b93bd68be7470fc95d2a9617a15de291b"}, @@ -7530,6 +7787,7 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -7545,6 +7803,7 @@ description = "digitalocean.com API to manage Droplets and Images" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "python-digitalocean-1.17.0.tar.gz", hash = "sha256:107854fde1aafa21774e8053cf253b04173613c94531f75d5a039ad770562b24"}, {file = "python_digitalocean-1.17.0-py3-none-any.whl", hash = "sha256:0032168e022e85fca314eb3f8dfaabf82087f2ed40839eb28f1eeeeca5afb1fa"}, @@ -7561,6 +7820,7 @@ description = "Saml Python Toolkit. Add SAML support to your Python software usi optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "python3-saml-1.16.0.tar.gz", hash = "sha256:97c9669aecabc283c6e5fb4eb264f446b6e006f5267d01c9734f9d8bffdac133"}, {file = "python3_saml-1.16.0-py2-none-any.whl", hash = "sha256:c49097863c278ff669a337a96c46dc1f25d16307b4bb2679d2d1733cc4f5176a"}, @@ -7582,6 +7842,7 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, @@ -7616,7 +7877,7 @@ files = [ {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, ] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} +markers = {main = "(sys_platform == \"win32\" or platform_system == \"Windows\") and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\")", dev = "sys_platform == \"win32\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\")"} [[package]] name = "pyyaml" @@ -7625,6 +7886,7 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -7708,6 +7970,7 @@ description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b"}, {file = "redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c"}, @@ -7729,6 +7992,7 @@ description = "JSON Referencing + Python" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, @@ -7746,6 +8010,7 @@ description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, @@ -7887,6 +8152,7 @@ description = "The Reportlab Toolkit" optional = false python-versions = "<4,>=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd"}, {file = "reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378"}, @@ -7910,6 +8176,7 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -7933,6 +8200,7 @@ description = "File transport adapter for Requests" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2"}, {file = "requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576"}, @@ -7948,6 +8216,7 @@ description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -7967,6 +8236,7 @@ description = "Import exceptions from potentially bundled packages in requests." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "requestsexceptions-1.4.0-py2.py3-none-any.whl", hash = "sha256:3083d872b6e07dc5c323563ef37671d992214ad9a32b0ca4a3d7f5500bf38ce3"}, {file = "requestsexceptions-1.4.0.tar.gz", hash = "sha256:b095cbc77618f066d459a02b137b020c37da9f46d9b057704019c9f77dba3065"}, @@ -7979,6 +8249,7 @@ description = "Retrying" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, @@ -7991,6 +8262,7 @@ description = "Render rich text, tables, progress bars, syntax highlighting, mar optional = false python-versions = ">=3.8.0" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, @@ -8010,6 +8282,7 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, @@ -8135,6 +8408,7 @@ description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -8150,6 +8424,7 @@ description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip pres optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, @@ -8168,6 +8443,7 @@ description = "An extremely fast Python linter and code formatter, written in Ru optional = false python-versions = ">=3.7" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, @@ -8196,6 +8472,7 @@ description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456"}, {file = "s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125"}, @@ -8214,6 +8491,7 @@ description = "Scan dependencies for known vulnerabilities and licenses." optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf"}, {file = "safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e"}, @@ -8250,6 +8528,7 @@ description = "Schemas for Safety tools" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44"}, {file = "safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e"}, @@ -8269,6 +8548,7 @@ description = "Scaleway SDK for Python" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "scaleway-2.10.3-py3-none-any.whl", hash = "sha256:dbf381440d6caf37c878cf16445a63f4969a4aac2257c9b72c744d10ff223a0c"}, {file = "scaleway-2.10.3.tar.gz", hash = "sha256:b1f9dd1b1450767205234c6f5a345e5e25dc039c780253d698893b5c344ce594"}, @@ -8284,6 +8564,7 @@ description = "Scaleway SDK for Python" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "scaleway_core-2.10.3-py3-none-any.whl", hash = "sha256:fd4112144554d6adae22ff737555eeb0e38cb1063250b3e88c9aebc1b957793b"}, {file = "scaleway_core-2.10.3.tar.gz", hash = "sha256:56432f755d694669429de51d51c1d0b3361b28dc2f939b28e4cb954610ee76be"}, @@ -8301,6 +8582,7 @@ description = "Simple data validation library" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"}, {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"}, @@ -8316,6 +8598,7 @@ description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "sentry_sdk-2.51.0-py2.py3-none-any.whl", hash = "sha256:e21016d318a097c2b617bb980afd9fc737e1efc55f9b4f0cdc819982c9717d5f"}, {file = "sentry_sdk-2.51.0.tar.gz", hash = "sha256:b89d64577075fd8c13088bc3609a2ce77a154e5beb8cba7cc16560b0539df4f7"}, @@ -8380,6 +8663,7 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173"}, {file = "setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70"}, @@ -8401,6 +8685,7 @@ description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -8413,6 +8698,7 @@ description = "Python library and command-line utility for Shodan (https://devel optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6"}, ] @@ -8432,6 +8718,7 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -8444,6 +8731,7 @@ description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "slack_sdk-3.39.0-py2.py3-none-any.whl", hash = "sha256:b1556b2f5b8b12b94e5ea3f56c4f2c7f04462e4e1013d325c5764ff118044fa8"}, {file = "slack_sdk-3.39.0.tar.gz", hash = "sha256:6a56be10dc155c436ff658c6b776e1c082e29eae6a771fccf8b0a235822bbcb1"}, @@ -8459,6 +8747,7 @@ description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -8471,6 +8760,7 @@ description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, @@ -8487,6 +8777,7 @@ description = "A simple statsd client." optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093"}, {file = "statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128"}, @@ -8499,6 +8790,7 @@ description = "std-uritemplate implementation for Python" optional = false python-versions = "<4.0,>=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "std_uritemplate-2.0.8-py3-none-any.whl", hash = "sha256:839807a7f9d07f0bad1a88977c3428bd97b9ff0d229412a0bf36123d8c724257"}, {file = "std_uritemplate-2.0.8.tar.gz", hash = "sha256:138ceff2c5bfef18a650372a5e8c82fe7f780c87235513de6c342fb5f7e18347"}, @@ -8511,6 +8803,7 @@ description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.10" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820"}, {file = "stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945"}, @@ -8523,6 +8816,7 @@ description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -8538,6 +8832,7 @@ description = "Retry code until it succeeds" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, @@ -8554,6 +8849,7 @@ description = "Accurately separates a URL's subdomain, domain, and public suffix optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9"}, {file = "tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb"}, @@ -8576,6 +8872,7 @@ description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, @@ -8588,6 +8885,7 @@ description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -8610,6 +8908,7 @@ description = "Typer, build great CLIs. Easy to code. Based on Python type hints optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01"}, {file = "typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d"}, @@ -8628,6 +8927,7 @@ description = "Type annotations for aiobotocore ECR 3.1.1 service generated with optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "types_aiobotocore_ecr-3.1.1-py3-none-any.whl", hash = "sha256:e5c02e06ff057bbe7821fb40ac7de67d2335fdc7987ea31392051efe81ceb69c"}, {file = "types_aiobotocore_ecr-3.1.1.tar.gz", hash = "sha256:155edc63c612e1a7861fa746376a5143cc4f3ca05b60c27d68ced23e8567a344"}, @@ -8643,6 +8943,7 @@ description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -8655,6 +8956,7 @@ description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, @@ -8674,7 +8976,7 @@ files = [ {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] -markers = {dev = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"", dev = "sys_platform == \"win32\" and (platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\")"} [[package]] name = "tzlocal" @@ -8683,6 +8985,7 @@ description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, @@ -8701,6 +9004,7 @@ description = "Implementation of RFC 6570 URI Templates" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, @@ -8713,6 +9017,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, @@ -8731,6 +9036,7 @@ description = "New time-based UUID formats which are suited for use as a databas optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, @@ -8743,6 +9049,7 @@ description = "Python promises." optional = false python-versions = ">=3.6" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, @@ -8755,6 +9062,7 @@ description = "Find dead code" optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9"}, {file = "vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415"}, @@ -8767,6 +9075,7 @@ description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e"}, {file = "wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091"}, @@ -8779,6 +9088,7 @@ description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, @@ -8796,6 +9106,7 @@ description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"}, {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"}, @@ -8814,6 +9125,7 @@ description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, @@ -8905,6 +9217,7 @@ description = "A Python module for creating Excel XLSX files." optional = false python-versions = ">=3.8" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3"}, {file = "xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c"}, @@ -8917,6 +9230,7 @@ description = "Python bindings for the XML Security Library" optional = false python-versions = ">=3.5" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "xmlsec-1.3.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4dea6df3ffcb65d0b215678c3a0fe7bbc66785d6eae81291296e372498bad43a"}, {file = "xmlsec-1.3.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa1311f7489d050dde9028f5a2b5849c2927bb09c9a93491cb2f28fdc563912"}, @@ -8988,6 +9302,7 @@ description = "Makes working with XML feel like you are working with JSON" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d"}, {file = "xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649"}, @@ -9003,6 +9318,7 @@ description = "Yet another URL library" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, @@ -9148,6 +9464,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, @@ -9168,6 +9485,7 @@ description = "Very basic event publishing system" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0"}, {file = "zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0"}, @@ -9184,6 +9502,7 @@ description = "Interfaces for Python" optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "zope_interface-8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:788c293f3165964ec6527b2d861072c68eef53425213f36d3893ebee89a89623"}, {file = "zope_interface-8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9a4e785097e741a1c953b3970ce28f2823bd63c00adc5d276f2981dd66c96c15"}, @@ -9230,6 +9549,7 @@ description = "ZSTD Bindings for Python" optional = false python-versions = "*" groups = ["main"] +markers = "platform_system == \"Windows\" or platform_system == \"Darwin\" or platform_system == \"Linux\" or platform_system != \"Windows\" and platform_system != \"Darwin\" and platform_system != \"Linux\" or sys_platform != \"win32\"" files = [ {file = "zstd-1.5.7.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e72b353870286648a63261437b75f297e2967a26f210da4dfa4c08949935de7a"}, {file = "zstd-1.5.7.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:26aff5f24caeffde35f1b757499e935bc60a8e0d9e1ea8bde05dcf7d53df9325"}, @@ -9400,4 +9720,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.13" -content-hash = "bada7223d576ddd48ff74aa101d18e7465492cf014006e17354dbe2190a02b29" +content-hash = "6f5b09460c787d4890c48171fe03f7dc123c70a4f3552a63a4d2cb05748286a9" diff --git a/api/pyproject.toml b/api/pyproject.toml index 7fa01a062a..22bd72d822 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "drf-spectacular-jsonapi==0.5.1", "gunicorn==23.0.0", "lxml==5.3.2", - "prowler @ git+https://github.com/prowler-cloud/prowler.git@master", + "prowler @ git+https://github.com/prowler-cloud/prowler.git@feat/PROWLER-940-stage-2-a-image-provider-api-support", "psycopg2-binary==2.9.9", "pytest-celery[redis] (>=1.0.1,<2.0.0)", "sentry-sdk[django] (>=2.20.0,<3.0.0)", @@ -49,7 +49,7 @@ name = "prowler-api" package-mode = false # Needed for the SDK compatibility requires-python = ">=3.11,<3.13" -version = "1.19.0" +version = "1.20.0" [project.scripts] celery = "src.backend.config.settings.celery" diff --git a/api/src/backend/api/migrations/0076_image_provider.py b/api/src/backend/api/migrations/0076_image_provider.py new file mode 100644 index 0000000000..234b10ea37 --- /dev/null +++ b/api/src/backend/api/migrations/0076_image_provider.py @@ -0,0 +1,39 @@ +# Generated by Django migration for Image provider support + +from django.db import migrations + +import api.db_utils + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0075_cloudflare_provider"), + ] + + operations = [ + migrations.AlterField( + model_name="provider", + name="provider", + field=api.db_utils.ProviderEnumField( + choices=[ + ("aws", "AWS"), + ("azure", "Azure"), + ("gcp", "GCP"), + ("kubernetes", "Kubernetes"), + ("m365", "M365"), + ("github", "GitHub"), + ("mongodbatlas", "MongoDB Atlas"), + ("iac", "IaC"), + ("oraclecloud", "Oracle Cloud Infrastructure"), + ("alibabacloud", "Alibaba Cloud"), + ("cloudflare", "Cloudflare"), + ("image", "Image"), + ], + default="aws", + ), + ), + migrations.RunSQL( + "ALTER TYPE provider ADD VALUE IF NOT EXISTS 'image';", + reverse_sql=migrations.RunSQL.noop, + ), + ] diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 4bda51761c..f7bdebfbd0 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -288,6 +288,7 @@ class Provider(RowLevelSecurityProtectedModel): ORACLECLOUD = "oraclecloud", _("Oracle Cloud Infrastructure") ALIBABACLOUD = "alibabacloud", _("Alibaba Cloud") CLOUDFLARE = "cloudflare", _("Cloudflare") + IMAGE = "image", _("Image") @staticmethod def validate_aws_uid(value): @@ -410,6 +411,26 @@ class Provider(RowLevelSecurityProtectedModel): pointer="/data/attributes/uid", ) + @staticmethod + def validate_image_uid(value): + pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?(/[a-zA-Z0-9._-]+)*/?$" + if not re.match(pattern, value): + raise ModelValidationError( + detail="Image provider ID must be a valid registry URL " + "(e.g., docker.io, ghcr.io, registry.example.com:5000).", + code="image-uid", + pointer="/data/attributes/uid", + ) + port_match = re.search(r":(\d{1,5})(?=/|$)", value) + if port_match: + port = int(port_match.group(1)) + if not 1 <= port <= 65535: + raise ModelValidationError( + detail="Port number must be between 1 and 65535.", + code="image-uid", + pointer="/data/attributes/uid", + ) + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) @@ -433,6 +454,8 @@ class Provider(RowLevelSecurityProtectedModel): def clean(self): super().clean() + if self.provider == self.ProviderChoices.IMAGE.value and self.uid: + self.uid = re.sub(r"^https?://", "", self.uid) getattr(self, f"validate_{self.provider}_uid")(self.uid) def save(self, *args, **kwargs): @@ -681,8 +704,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): update_tag = models.BigIntegerField( null=True, blank=True, help_text="Cartography update tag (epoch)" ) - graph_database = models.CharField(max_length=63, null=True, blank=True) - is_graph_database_deleted = models.BooleanField(default=False) ingestion_exceptions = models.JSONField(default=dict, null=True, blank=True) class Meta(RowLevelSecurityProtectedModel.Meta): @@ -709,21 +730,6 @@ class AttackPathsScan(RowLevelSecurityProtectedModel): fields=["tenant_id", "scan_id"], name="aps_scan_lookup_idx", ), - models.Index( - fields=["tenant_id", "provider_id"], - name="aps_active_graph_idx", - include=["graph_database", "id"], - condition=Q(is_graph_database_deleted=False), - ), - models.Index( - fields=["tenant_id", "provider_id", "-completed_at"], - name="aps_completed_graph_idx", - include=["graph_database", "id"], - condition=Q( - state=StateChoices.COMPLETED, - is_graph_database_deleted=False, - ), - ), ] class JSONAPIMeta: diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 778233ddab..f3fdcb0b63 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.19.0 + version: 1.20.0 description: |- Prowler API specification. @@ -616,7 +616,7 @@ paths: operationId: attack_paths_scans_queries_retrieve description: Retrieve the catalog of Attack Paths queries available for this Attack Paths scan. - summary: List attack paths queries + summary: List Attack Paths queries parameters: - in: query name: fields[attack-paths-scans] @@ -714,7 +714,7 @@ paths: description: Bad request (e.g., Unknown Attack Paths query for the selected provider) '404': - description: No attack paths found for the given query and parameters + description: No Attack Paths found for the given query and parameters '500': description: Attack Paths query execution failed due to a database error /api/v1/compliance-overviews: @@ -12438,6 +12438,8 @@ components: type: string name: type: string + short_description: + type: string description: type: string provider: @@ -12446,12 +12448,42 @@ components: type: array items: $ref: '#/components/schemas/AttackPathsQueryParameter' + attribution: + allOf: + - $ref: '#/components/schemas/AttackPathsQueryAttribution' + nullable: true required: - id - name + - short_description - description - provider - parameters + AttackPathsQueryAttribution: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - attack-paths-query-attributions + id: {} + attributes: + type: object + properties: + text: + type: string + link: + type: string + required: + - text + - link AttackPathsQueryParameter: type: object required: @@ -17316,7 +17348,8 @@ components: required: - api_key - api_email - writeOnly: true + - type: object + - type: object required: - secret relationships: @@ -19346,7 +19379,8 @@ components: required: - api_key - api_email - writeOnly: true + - type: object + - type: object required: - secret_type - secret @@ -19732,7 +19766,8 @@ components: required: - api_key - api_email - writeOnly: true + - type: object + - type: object required: - secret_type - secret @@ -20130,7 +20165,8 @@ components: required: - api_key - api_email - writeOnly: true + - type: object + - type: object required: - secret relationships: diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 56f98a5401..30a8cad99b 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -24,6 +24,7 @@ from prowler.providers.cloudflare.cloudflare_provider import CloudflareProvider from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.github.github_provider import GithubProvider from prowler.providers.iac.iac_provider import IacProvider +from prowler.providers.image.image_provider import ImageProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.mongodbatlas.mongodbatlas_provider import MongodbatlasProvider @@ -120,6 +121,7 @@ class TestReturnProwlerProvider: (Provider.ProviderChoices.IAC.value, IacProvider), (Provider.ProviderChoices.ALIBABACLOUD.value, AlibabacloudProvider), (Provider.ProviderChoices.CLOUDFLARE.value, CloudflareProvider), + (Provider.ProviderChoices.IMAGE.value, ImageProvider), ], ) def test_return_prowler_provider(self, provider_type, expected_provider): @@ -186,6 +188,54 @@ class TestProwlerProviderConnectionTest: assert isinstance(connection.error, Provider.secret.RelatedObjectDoesNotExist) assert str(connection.error) == "Provider has no secret." + @patch( + "prowler.providers.image.lib.registry.factory.create_registry_adapter", + ) + @patch("api.utils.return_prowler_provider") + def test_prowler_provider_connection_test_image_success( + self, mock_return_prowler_provider, mock_create_adapter + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.uid = "ghcr.io" + provider.secret.secret = {"registry_token": "tok"} + + mock_adapter = MagicMock() + mock_create_adapter.return_value = mock_adapter + + connection = prowler_provider_connection_test(provider) + + assert connection.is_connected is True + assert connection.error is None + mock_create_adapter.assert_called_once_with( + registry_url="ghcr.io", + username=None, + password=None, + token="tok", + ) + mock_adapter.list_repositories.assert_called_once() + + @patch( + "prowler.providers.image.lib.registry.factory.create_registry_adapter", + ) + @patch("api.utils.return_prowler_provider") + def test_prowler_provider_connection_test_image_failure( + self, mock_return_prowler_provider, mock_create_adapter + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.uid = "ghcr.io" + provider.secret.secret = {"registry_token": "bad-token"} + + mock_adapter = MagicMock() + mock_adapter.list_repositories.side_effect = Exception("401 Unauthorized") + mock_create_adapter.return_value = mock_adapter + + connection = prowler_provider_connection_test(provider) + + assert connection.is_connected is False + assert connection.error == "401 Unauthorized" + class TestGetProwlerProviderKwargs: @pytest.mark.parametrize( @@ -330,6 +380,103 @@ class TestGetProwlerProviderKwargs: } assert result == expected_result + def test_get_prowler_provider_kwargs_image_provider(self): + """Test that Image provider gets correct kwargs with registry URL and auth.""" + provider_uid = "ghcr.io" + secret_dict = { + "registry_username": "user", + "registry_password": "pass", + } + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = { + "registry": provider_uid, + "registry_username": "user", + "registry_password": "pass", + } + assert result == expected_result + + def test_get_prowler_provider_kwargs_image_provider_with_filters(self): + """Test that Image provider includes scan filters.""" + provider_uid = "docker.io" + secret_dict = { + "registry_token": "ghp_abc123", + "image_filter": "my-app.*", + "tag_filter": "v[0-9]+", + "max_images": 50, + } + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = { + "registry": provider_uid, + "registry_token": "ghp_abc123", + "image_filter": "my-app.*", + "tag_filter": "v[0-9]+", + "max_images": 50, + } + assert result == expected_result + + def test_get_prowler_provider_kwargs_image_provider_no_auth(self): + """Test that Image provider works with empty secret for public registries.""" + provider_uid = "docker.io" + secret_dict = {} + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = {"registry": provider_uid} + assert result == expected_result + + def test_get_prowler_provider_kwargs_image_provider_ignores_mutelist(self): + """Test that Image provider does NOT receive mutelist_content. + + Image provider uses Trivy's built-in logic, so it should not + receive mutelist_content even when a mutelist processor is configured. + """ + provider_uid = "ghcr.io" + secret_dict = {"registry_token": "test_token"} + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + mutelist_processor = MagicMock() + mutelist_processor.configuration = {"Mutelist": {"key": "value"}} + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.IMAGE.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider, mutelist_processor) + + assert "mutelist_content" not in result + expected_result = { + "registry": provider_uid, + "registry_token": "test_token", + } + assert result == expected_result + def test_get_prowler_provider_kwargs_unsupported_provider(self): # Setup provider_uid = "provider_uid" diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 4bb7509bc7..c7157577a1 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1179,6 +1179,26 @@ class TestProviderViewSet: "uid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "alias": "Cloudflare Account", }, + { + "provider": "image", + "uid": "ghcr.io", + "alias": "GitHub Registry", + }, + { + "provider": "image", + "uid": "docker.io", + "alias": "Docker Hub", + }, + { + "provider": "image", + "uid": "123456789012.dkr.ecr.us-east-1.amazonaws.com", + "alias": "ECR", + }, + { + "provider": "image", + "uid": "https://registry.example.com:5000", + "alias": "Private", + }, ] ), ) @@ -1598,6 +1618,26 @@ class TestProviderViewSet: "cloudflare-uid", "uid", ), + # Image UID validation - too short (below min_length) + ( + { + "provider": "image", + "uid": "ab", + "alias": "test", + }, + "min_length", + "uid", + ), + # Image UID validation - invalid characters (space) + ( + { + "provider": "image", + "uid": "not valid!", + "alias": "test", + }, + "image-uid", + "uid", + ), ] ), ) @@ -1753,6 +1793,38 @@ class TestProviderViewSet: assert "Content-Location" in response.headers assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}" + @patch("api.v1.views.Task.objects.get") + @patch("api.v1.views.check_provider_connection_task.delay") + def test_providers_connection_image( + self, + mock_provider_connection, + mock_task_get, + authenticated_client, + 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 + + image_provider = providers_fixture[10] + assert image_provider.provider == "image" + assert image_provider.connected is None + assert image_provider.connection_last_checked_at is None + + response = authenticated_client.post( + reverse("provider-connection", kwargs={"pk": image_provider.id}) + ) + assert response.status_code == status.HTTP_202_ACCEPTED + mock_provider_connection.assert_called_once_with( + provider_id=str(image_provider.id), tenant_id=ANY + ) + assert "Content-Location" in response.headers + assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}" + def test_providers_connection_invalid_provider( self, authenticated_client, providers_fixture ): @@ -1775,17 +1847,17 @@ class TestProviderViewSet: ), ("alias", "aws_testing_1", 1), ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 10), + ("inserted_at", TODAY, 11), ( "inserted_at.gte", "2024-01-01", - 10, + 11, ), ("inserted_at.lte", "2024-01-01", 0), ( "updated_at.gte", "2024-01-01", - 10, + 11, ), ("updated_at.lte", "2024-01-01", 0), ] @@ -2392,6 +2464,39 @@ class TestProviderSecretViewSet: "api_email": "user@example.com", }, ), + # Image with Docker login + ( + Provider.ProviderChoices.IMAGE.value, + ProviderSecret.TypeChoices.STATIC, + { + "registry_username": "user", + "registry_password": "pass", + }, + ), + # Image with token + ( + Provider.ProviderChoices.IMAGE.value, + ProviderSecret.TypeChoices.STATIC, + { + "registry_token": "ghp_abc123", + }, + ), + # Image with no auth + filters + ( + Provider.ProviderChoices.IMAGE.value, + ProviderSecret.TypeChoices.STATIC, + { + "image_filter": "my-app.*", + "tag_filter": "v[0-9]+", + "max_images": 50, + }, + ), + # Image with empty secret (public registry) + ( + Provider.ProviderChoices.IMAGE.value, + ProviderSecret.TypeChoices.STATIC, + {}, + ), ], ) def test_provider_secrets_create_valid( @@ -3830,6 +3935,7 @@ class TestAttackPathsScanViewSet: AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -3887,11 +3993,11 @@ class TestAttackPathsScanViewSet: attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], - graph_database="tenant-db", ) query_definition = AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -3917,10 +4023,16 @@ class TestAttackPathsScanViewSet: ], } + expected_db_name = f"db-tenant-{attack_paths_scan.provider.tenant_id}" + with ( patch( "api.v1.views.get_query_by_id", return_value=query_definition ) as mock_get_query, + patch( + "api.v1.views.graph_database.get_database_name", + return_value=expected_db_name, + ) as mock_get_db_name, patch( "api.v1.views.attack_paths_views_helpers.prepare_query_parameters", return_value=prepared_parameters, @@ -3942,17 +4054,18 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_200_OK mock_get_query.assert_called_once_with("aws-rds") + mock_get_db_name.assert_called_once_with(attack_paths_scan.provider.tenant_id) mock_prepare.assert_called_once_with( query_definition, {}, attack_paths_scan.provider.uid, ) mock_execute.assert_called_once_with( - attack_paths_scan, + expected_db_name, query_definition, prepared_parameters, ) - mock_clear_cache.assert_called_once_with(attack_paths_scan.graph_database) + mock_clear_cache.assert_called_once_with(expected_db_name) result = response.json()["data"] attributes = result["attributes"] assert attributes["nodes"] == graph_payload["nodes"] @@ -3983,31 +4096,6 @@ class TestAttackPathsScanViewSet: assert response.status_code == status.HTTP_400_BAD_REQUEST assert "must be completed" in response.json()["errors"][0]["detail"] - def test_run_attack_paths_query_requires_graph_database( - self, - authenticated_client, - providers_fixture, - scans_fixture, - create_attack_paths_scan, - ): - provider = providers_fixture[0] - attack_paths_scan = create_attack_paths_scan( - provider, - scan=scans_fixture[0], - graph_database=None, - ) - - response = authenticated_client.post( - reverse( - "attack-paths-scans-queries-run", kwargs={"pk": attack_paths_scan.id} - ), - data=self._run_payload(), - content_type=API_JSON_CONTENT_TYPE, - ) - - assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR - assert "does not reference a graph database" in str(response.json()) - def test_run_attack_paths_query_unknown_query( self, authenticated_client, @@ -4049,6 +4137,7 @@ class TestAttackPathsScanViewSet: query_definition = AttackPathsQueryDefinition( id="aws-empty", name="empty", + short_description="", description="", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -10841,25 +10930,20 @@ class TestTenantFinishACSView: assert "sso_saml_failed=true" in response.url def test_dispatch_skips_role_mapping_when_single_manage_account_user( - self, create_test_user, tenants_fixture, saml_setup, settings, monkeypatch + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + saml_setup, + settings, + monkeypatch, ): """Test that role mapping is skipped when tenant has only one user with MANAGE_ACCOUNT role""" monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") user = create_test_user tenant = tenants_fixture[0] - # Create a single role with manage_account=True for the user - admin_role = Role.objects.using(MainRouter.admin_db).create( - name="admin", - tenant=tenant, - manage_account=True, - manage_users=True, - manage_billing=True, - manage_providers=True, - manage_integrations=True, - manage_scans=True, - unlimited_visibility=True, - ) + admin_role = admin_role_fixture UserRoleRelationship.objects.using(MainRouter.admin_db).create( user=user, role=admin_role, tenant_id=tenant.id ) @@ -10930,35 +11014,26 @@ class TestTenantFinishACSView: .exists() ) - def test_dispatch_applies_role_mapping_when_multiple_manage_account_users( - self, create_test_user, tenants_fixture, saml_setup, settings, monkeypatch + def test_dispatch_skips_role_mapping_when_last_manage_account_user_maps_to_existing_role( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, ): - """Test that role mapping is applied when tenant has multiple users with MANAGE_ACCOUNT role""" + """Test that role mapping is skipped when it would remove the last MANAGE_ACCOUNT user""" monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") user = create_test_user tenant = tenants_fixture[0] - # Create a second user with manage_account=True - second_admin = User.objects.using(MainRouter.admin_db).create( - email="admin2@prowler.com", name="Second Admin" - ) - admin_role = Role.objects.using(MainRouter.admin_db).create( - name="admin", - tenant=tenant, - manage_account=True, - manage_users=True, - manage_billing=True, - manage_providers=True, - manage_integrations=True, - manage_scans=True, - unlimited_visibility=True, - ) + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] UserRoleRelationship.objects.using(MainRouter.admin_db).create( user=user, role=admin_role, tenant_id=tenant.id ) - UserRoleRelationship.objects.using(MainRouter.admin_db).create( - user=second_admin, role=admin_role, tenant_id=tenant.id - ) social_account = SocialAccount( user=user, @@ -10967,7 +11042,7 @@ class TestTenantFinishACSView: "firstName": ["John"], "lastName": ["Doe"], "organization": ["testing_company"], - "userType": ["viewer"], # This SHOULD be applied + "userType": [viewer_role.name], }, ) @@ -11005,10 +11080,91 @@ class TestTenantFinishACSView: assert response.status_code == 302 - # Verify the viewer role was created and assigned (role mapping was applied) - viewer_role = Role.objects.using(MainRouter.admin_db).get( - name="viewer", tenant=tenant + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=user, role=admin_role, tenant_id=tenant.id) + .exists() ) + assert not ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=user, role=viewer_role, tenant_id=tenant.id) + .exists() + ) + + def test_dispatch_applies_role_mapping_when_multiple_manage_account_users( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, + ): + """Test that role mapping is applied when tenant has multiple users with MANAGE_ACCOUNT role""" + monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") + user = create_test_user + tenant = tenants_fixture[0] + + # Create a second user with manage_account=True + second_admin = User.objects.using(MainRouter.admin_db).create( + email="admin2@prowler.com", name="Second Admin" + ) + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=user, role=admin_role, tenant_id=tenant.id + ) + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=second_admin, role=admin_role, tenant_id=tenant.id + ) + + social_account = SocialAccount( + user=user, + provider="saml", + extra_data={ + "firstName": ["John"], + "lastName": ["Doe"], + "organization": ["testing_company"], + "userType": [viewer_role.name], # This SHOULD be applied + }, + ) + + request = RequestFactory().get( + reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) + ) + request.user = user + request.session = {} + + with ( + patch( + "allauth.socialaccount.providers.saml.views.get_app_or_404" + ) as mock_get_app_or_404, + patch( + "allauth.socialaccount.models.SocialApp.objects.get" + ) as mock_socialapp_get, + patch( + "allauth.socialaccount.models.SocialAccount.objects.get" + ) as mock_sa_get, + patch("api.models.SAMLDomainIndex.objects.get") as mock_saml_domain_get, + patch("api.models.SAMLConfiguration.objects.get") as mock_saml_config_get, + patch("api.models.User.objects.get") as mock_user_get, + ): + mock_get_app_or_404.return_value = MagicMock( + provider="saml", client_id="testtenant", name="Test App", settings={} + ) + mock_sa_get.return_value = social_account + mock_socialapp_get.return_value = MagicMock(provider_id="saml") + mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id) + mock_saml_config_get.return_value = MagicMock() + mock_user_get.return_value = user + + view = TenantFinishACSView.as_view() + response = view(request, organization_slug="testtenant") + + assert response.status_code == 302 + + # Verify the viewer role was assigned (role mapping was applied) assert ( UserRoleRelationship.objects.using(MainRouter.admin_db) .filter(user=user, role=viewer_role, tenant_id=tenant.id) @@ -11022,6 +11178,86 @@ class TestTenantFinishACSView: .exists() ) + def test_dispatch_applies_role_mapping_for_non_admin_user_with_single_admin( + self, + create_test_user, + tenants_fixture, + admin_role_fixture, + roles_fixture, + saml_setup, + settings, + monkeypatch, + ): + """Test that role mapping is applied for a non-admin user when a single admin exists""" + monkeypatch.setenv("SAML_SSO_CALLBACK_URL", "http://localhost/sso-complete") + admin_user = create_test_user + tenant = tenants_fixture[0] + non_admin_user = User.objects.using(MainRouter.admin_db).create( + email="viewer@prowler.com", name="Viewer" + ) + + admin_role = admin_role_fixture + viewer_role = roles_fixture[3] + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=admin_user, role=admin_role, tenant_id=tenant.id + ) + + social_account = SocialAccount( + user=non_admin_user, + provider="saml", + extra_data={ + "firstName": ["Jane"], + "lastName": ["Doe"], + "organization": ["testing_company"], + "userType": [viewer_role.name], + }, + ) + + request = RequestFactory().get( + reverse("saml_finish_acs", kwargs={"organization_slug": "testtenant"}) + ) + request.user = non_admin_user + request.session = {} + + with ( + patch( + "allauth.socialaccount.providers.saml.views.get_app_or_404" + ) as mock_get_app_or_404, + patch( + "allauth.socialaccount.models.SocialApp.objects.get" + ) as mock_socialapp_get, + patch( + "allauth.socialaccount.models.SocialAccount.objects.get" + ) as mock_sa_get, + patch("api.models.SAMLDomainIndex.objects.get") as mock_saml_domain_get, + patch("api.models.SAMLConfiguration.objects.get") as mock_saml_config_get, + patch("api.models.User.objects.get") as mock_user_get, + ): + mock_get_app_or_404.return_value = MagicMock( + provider="saml", client_id="testtenant", name="Test App", settings={} + ) + mock_sa_get.return_value = social_account + mock_socialapp_get.return_value = MagicMock(provider_id="saml") + mock_saml_domain_get.return_value = SimpleNamespace(tenant_id=tenant.id) + mock_saml_config_get.return_value = MagicMock() + mock_user_get.return_value = non_admin_user + + view = TenantFinishACSView.as_view() + response = view(request, organization_slug="testtenant") + + assert response.status_code == 302 + + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=non_admin_user, role=viewer_role, tenant_id=tenant.id) + .exists() + ) + assert ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(user=admin_user, role=admin_role, tenant_id=tenant.id) + .exists() + ) + @pytest.mark.django_db class TestLighthouseConfigViewSet: diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 343537efa9..d5173c2c1a 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.github.github_provider import GithubProvider from prowler.providers.iac.iac_provider import IacProvider + from prowler.providers.image.image_provider import ImageProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.mongodbatlas.mongodbatlas_provider import ( @@ -78,9 +79,11 @@ def return_prowler_provider( AlibabacloudProvider | AwsProvider | AzureProvider + | CloudflareProvider | GcpProvider | GithubProvider | IacProvider + | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider @@ -92,7 +95,7 @@ def return_prowler_provider( provider (Provider): The provider object containing the provider type and associated secrets. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class. + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: The corresponding provider class. Raises: ValueError: If the provider type specified in `provider.provider` is not supported. @@ -152,6 +155,10 @@ def return_prowler_provider( ) prowler_provider = CloudflareProvider + case Provider.ProviderChoices.IMAGE.value: + from prowler.providers.image.image_provider import ImageProvider + + prowler_provider = ImageProvider case _: raise ValueError(f"Provider type {provider.provider} not supported") return prowler_provider @@ -208,11 +215,30 @@ def get_prowler_provider_kwargs( **prowler_provider_kwargs, "filter_accounts": [provider.uid], } + elif provider.provider == Provider.ProviderChoices.IMAGE.value: + prowler_provider_kwargs = { + "registry": provider.uid, + } + secret = provider.secret.secret + for key in ( + "registry_username", + "registry_password", + "registry_token", + "image_filter", + "tag_filter", + ): + if key in secret: + prowler_provider_kwargs[key] = secret[key] + if "max_images" in secret: + prowler_provider_kwargs["max_images"] = int(secret["max_images"]) if mutelist_processor: mutelist_content = mutelist_processor.configuration.get("Mutelist", {}) - # IaC provider doesn't support mutelist (uses Trivy's built-in logic) - if mutelist_content and provider.provider != Provider.ProviderChoices.IAC.value: + # IaC and Image providers don't support mutelist (Trivy handles its own logic) + if mutelist_content and provider.provider not in ( + Provider.ProviderChoices.IAC.value, + Provider.ProviderChoices.IMAGE.value, + ): prowler_provider_kwargs["mutelist_content"] = mutelist_content return prowler_provider_kwargs @@ -229,6 +255,7 @@ def initialize_prowler_provider( | GcpProvider | GithubProvider | IacProvider + | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider @@ -241,7 +268,7 @@ def initialize_prowler_provider( mutelist_processor (Processor): The mutelist processor object containing the mutelist configuration. Returns: - AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class + AlibabacloudProvider | AwsProvider | AzureProvider | CloudflareProvider | GcpProvider | GithubProvider | IacProvider | ImageProvider | KubernetesProvider | M365Provider | MongodbatlasProvider | OraclecloudProvider: An instance of the corresponding provider class initialized with the provider's secrets. """ prowler_provider = return_prowler_provider(provider) @@ -276,6 +303,20 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: if "access_token" in prowler_provider_kwargs: iac_test_kwargs["access_token"] = prowler_provider_kwargs["access_token"] return prowler_provider.test_connection(**iac_test_kwargs) + elif provider.provider == Provider.ProviderChoices.IMAGE.value: + from prowler.providers.image.lib.registry.factory import create_registry_adapter + + try: + adapter = create_registry_adapter( + registry_url=provider.uid, + username=prowler_provider_kwargs.get("registry_username"), + password=prowler_provider_kwargs.get("registry_password"), + token=prowler_provider_kwargs.get("registry_token"), + ) + adapter.list_repositories() + return Connection(is_connected=True, error=None) + except Exception as e: + return Connection(is_connected=False, error=str(e)) else: return prowler_provider.test_connection( **prowler_provider_kwargs, diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 7b34b06d2b..0c2c5acb19 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -373,6 +373,37 @@ from rest_framework_json_api import serializers }, "required": ["api_key", "api_email"], }, + { + "type": "object", + "title": "Image Registry Credentials", + "properties": { + "registry_username": { + "type": "string", + "description": "Username for Docker login authentication.", + }, + "registry_password": { + "type": "string", + "description": "Password for Docker login authentication.", + }, + "registry_token": { + "type": "string", + "description": "Bearer token for registry authentication.", + }, + "image_filter": { + "type": "string", + "description": "Regex pattern to filter repository names during registry enumeration.", + }, + "tag_filter": { + "type": "string", + "description": "Regex pattern to filter image tags during registry enumeration.", + }, + "max_images": { + "type": "integer", + "minimum": 0, + "description": "Maximum number of images to scan (0 = unlimited).", + }, + }, + }, ] } ) diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index e2ae7fc8d2..6386759113 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1176,6 +1176,14 @@ class AttackPathsScanSerializer(RLSSerializer): return provider.uid if provider else None +class AttackPathsQueryAttributionSerializer(BaseSerializerV1): + text = serializers.CharField() + link = serializers.CharField() + + class JSONAPIMeta: + resource_name = "attack-paths-query-attributions" + + class AttackPathsQueryParameterSerializer(BaseSerializerV1): name = serializers.CharField() label = serializers.CharField() @@ -1190,7 +1198,9 @@ class AttackPathsQueryParameterSerializer(BaseSerializerV1): class AttackPathsQuerySerializer(BaseSerializerV1): id = serializers.CharField() name = serializers.CharField() + short_description = serializers.CharField() description = serializers.CharField() + attribution = AttackPathsQueryAttributionSerializer(allow_null=True, required=False) provider = serializers.CharField() parameters = AttackPathsQueryParameterSerializer(many=True) @@ -1515,6 +1525,8 @@ class BaseWriteProviderSecretSerializer(BaseWriteSerializer): "or both 'api_key' and 'api_email'." } ) + elif provider_type == Provider.ProviderChoices.IMAGE.value: + serializer = ImageProviderSecret(data=secret) else: raise serializers.ValidationError( {"provider": f"Provider type not supported {provider_type}"} @@ -1653,6 +1665,37 @@ class IacProviderSecret(serializers.Serializer): resource_name = "provider-secrets" +class ImageProviderSecret(serializers.Serializer): + registry_username = serializers.CharField(required=False) + registry_password = serializers.CharField(required=False) + registry_token = serializers.CharField(required=False) + image_filter = serializers.CharField(required=False) + tag_filter = serializers.CharField(required=False) + max_images = serializers.IntegerField(required=False, min_value=0) + + def validate(self, attrs): + has_username = attrs.get("registry_username") + has_password = attrs.get("registry_password") + has_token = attrs.get("registry_token") + + if (has_username or has_password) and has_token: + raise serializers.ValidationError( + "You cannot provide both registry_username/registry_password and registry_token." + ) + if has_username and not has_password: + raise serializers.ValidationError( + "registry_password is required when registry_username is provided." + ) + if has_password and not has_username: + raise serializers.ValidationError( + "registry_username is required when registry_password is provided." + ) + return super().validate(attrs) + + class Meta: + resource_name = "provider-secrets" + + class OracleCloudProviderSecret(serializers.Serializer): user = serializers.CharField() fingerprint = serializers.CharField() diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index d807dfd8fe..bba1689f7d 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -392,7 +392,7 @@ class SchemaView(SpectacularAPIView): def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.19.0" + spectacular_settings.VERSION = "1.20.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -763,27 +763,40 @@ class TenantFinishACSView(FinishACSView): .tenant ) - # Check if tenant has only one user with MANAGE_ACCOUNT role - users_with_manage_account = ( + role_name = ( + extra.get("userType", ["no_permissions"])[0].strip() + if extra.get("userType") + else "no_permissions" + ) + role = ( + Role.objects.using(MainRouter.admin_db) + .filter(name=role_name, tenant=tenant) + .first() + ) + + # Only skip mapping if it would remove the last MANAGE_ACCOUNT user + remaining_manage_account_users = ( UserRoleRelationship.objects.using(MainRouter.admin_db) .filter(role__manage_account=True, tenant_id=tenant.id) + .exclude(user_id=user_id) .values("user") .distinct() .count() ) + user_has_manage_account = ( + UserRoleRelationship.objects.using(MainRouter.admin_db) + .filter(role__manage_account=True, tenant_id=tenant.id, user_id=user_id) + .exists() + ) + role_manage_account = role.manage_account if role else False + would_remove_last_manage_account = ( + user_has_manage_account + and remaining_manage_account_users == 0 + and not role_manage_account + ) - # Only apply role mapping from userType if tenant does NOT have exactly one user with MANAGE_ACCOUNT - if users_with_manage_account != 1: - role_name = ( - extra.get("userType", ["no_permissions"])[0].strip() - if extra.get("userType") - else "no_permissions" - ) - try: - role = Role.objects.using(MainRouter.admin_db).get( - name=role_name, tenant=tenant - ) - except Role.DoesNotExist: + if not would_remove_last_manage_account: + if role is None: role = Role.objects.using(MainRouter.admin_db).create( name=role_name, tenant=tenant, @@ -2415,15 +2428,6 @@ class AttackPathsScanViewSet(BaseRLSViewSet): } ) - if not attack_paths_scan.graph_database: - logger.error( - f"The Attack Paths Scan {attack_paths_scan.id} does not reference a graph database" - ) - return Response( - {"detail": "The Attack Paths scan does not reference a graph database"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - payload = attack_paths_views_helpers.normalize_run_payload(request.data) serializer = AttackPathsQueryRunRequestSerializer(data=payload) serializer.is_valid(raise_exception=True) @@ -2437,6 +2441,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): {"id": "Unknown Attack Paths query for the selected provider"} ) + database_name = graph_database.get_database_name( + attack_paths_scan.provider.tenant_id + ) parameters = attack_paths_views_helpers.prepare_query_parameters( query_definition, serializer.validated_data.get("parameters", {}), @@ -2444,9 +2451,9 @@ class AttackPathsScanViewSet(BaseRLSViewSet): ) graph = attack_paths_views_helpers.execute_attack_paths_query( - attack_paths_scan, query_definition, parameters + database_name, query_definition, parameters ) - graph_database.clear_cache(attack_paths_scan.graph_database) + graph_database.clear_cache(database_name) status_code = status.HTTP_200_OK if not graph.get("nodes"): diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index e50f92e1ab..74b2e4ac26 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -537,6 +537,12 @@ def providers_fixture(tenants_fixture): alias="cloudflare_testing", tenant_id=tenant.id, ) + provider11 = Provider.objects.create( + provider="image", + uid="ghcr.io", + alias="image_testing", + tenant_id=tenant.id, + ) return ( provider1, @@ -549,6 +555,7 @@ def providers_fixture(tenants_fixture): provider8, provider9, provider10, + provider11, ) @@ -1618,7 +1625,6 @@ def create_attack_paths_scan(): scan=None, state=StateChoices.COMPLETED, progress=0, - graph_database="tenant-db", **extra_fields, ): scan_instance = scan or Scan.objects.create( @@ -1635,7 +1641,6 @@ def create_attack_paths_scan(): "scan": scan_instance, "state": state, "progress": progress, - "graph_database": graph_database, } payload.update(extra_fields) @@ -1663,6 +1668,7 @@ def attack_paths_query_definition_factory(): definition_payload = { "id": "aws-test", "name": "Attack Paths Test Query", + "short_description": "Synthetic short description for tests.", "description": "Synthetic Attack Paths definition for tests.", "provider": "aws", "cypher": "RETURN 1", diff --git a/api/src/backend/tasks/jobs/export.py b/api/src/backend/tasks/jobs/export.py index 883d7d0dbe..f9e85a5901 100644 --- a/api/src/backend/tasks/jobs/export.py +++ b/api/src/backend/tasks/jobs/export.py @@ -35,6 +35,11 @@ from prowler.lib.outputs.compliance.cis.cis_github import GithubCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS from prowler.lib.outputs.compliance.cis.cis_m365 import M365CIS from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS +from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA +from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA +from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA +from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA +from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS @@ -90,6 +95,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS), (lambda name: name == "ccc_aws", CCC_AWS), (lambda name: name.startswith("c5_"), AWSC5), + (lambda name: name.startswith("csa_"), AWSCSA), ], "azure": [ (lambda name: name.startswith("cis_"), AzureCIS), @@ -99,6 +105,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "ccc_azure", CCC_Azure), (lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure), (lambda name: name == "c5_azure", AzureC5), + (lambda name: name.startswith("csa_"), AzureCSA), ], "gcp": [ (lambda name: name.startswith("cis_"), GCPCIS), @@ -108,6 +115,7 @@ COMPLIANCE_CLASS_MAP = { (lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP), (lambda name: name == "ccc_gcp", CCC_GCP), (lambda name: name == "c5_gcp", GCPC5), + (lambda name: name.startswith("csa_"), GCPCSA), ], "kubernetes": [ (lambda name: name.startswith("cis_"), KubernetesCIS), @@ -129,11 +137,17 @@ COMPLIANCE_CLASS_MAP = { # IaC provider doesn't have specific compliance frameworks yet # Trivy handles its own compliance checks ], + "image": [ + # Image provider doesn't have specific compliance frameworks yet + # Trivy handles its own compliance checks + ], "oraclecloud": [ (lambda name: name.startswith("cis_"), OracleCloudCIS), + (lambda name: name.startswith("csa_"), OracleCloudCSA), ], "alibabacloud": [ (lambda name: name.startswith("cis_"), AlibabaCloudCIS), + (lambda name: name.startswith("csa_"), AlibabaCloudCSA), ( lambda name: name == "prowler_threatscore_alibabacloud", ProwlerThreatScoreAlibaba, diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index 0dd3b0905b..1ee2352926 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -133,13 +133,41 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str) scan_id (str): The ID of the scan that was performed. provider_id (str): The primary key of the Provider instance that was scanned. """ - chain( - create_compliance_requirements_task.si(tenant_id=tenant_id, scan_id=scan_id), - update_provider_compliance_scores_task.si(tenant_id=tenant_id, scan_id=scan_id), - ).apply_async() - aggregate_attack_surface_task.apply_async( - kwargs={"tenant_id": tenant_id, "scan_id": scan_id} + with rls_transaction(tenant_id): + provider_type = Provider.objects.get(id=provider_id).provider + + has_compliance = provider_type not in ( + Provider.ProviderChoices.IAC.value, + Provider.ProviderChoices.IMAGE.value, ) + + if has_compliance: + chain( + create_compliance_requirements_task.si( + tenant_id=tenant_id, scan_id=scan_id + ), + update_provider_compliance_scores_task.si( + tenant_id=tenant_id, scan_id=scan_id + ), + ).apply_async() + aggregate_attack_surface_task.apply_async( + kwargs={"tenant_id": tenant_id, "scan_id": scan_id} + ) + + final_group_tasks = [ + check_integrations_task.si( + tenant_id=tenant_id, + provider_id=provider_id, + scan_id=scan_id, + ), + ] + if has_compliance: + final_group_tasks.append( + generate_compliance_reports_task.si( + tenant_id=tenant_id, scan_id=scan_id, provider_id=provider_id + ), + ) + chain( perform_scan_summary_task.si(tenant_id=tenant_id, scan_id=scan_id), group( @@ -148,17 +176,7 @@ def _perform_scan_complete_tasks(tenant_id: str, scan_id: str, provider_id: str) scan_id=scan_id, provider_id=provider_id, tenant_id=tenant_id ), ), - group( - # Use optimized task that generates both reports with shared queries - generate_compliance_reports_task.si( - tenant_id=tenant_id, scan_id=scan_id, provider_id=provider_id - ), - check_integrations_task.si( - tenant_id=tenant_id, - provider_id=provider_id, - scan_id=scan_id, - ), - ), + group(*final_group_tasks), ).apply_async() if can_provider_run_attack_paths_scan(tenant_id, provider_id): diff --git a/poetry.lock b/poetry.lock index b3fb2a314c..f4d82258e7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "about-time" @@ -5874,6 +5874,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, @@ -5882,6 +5883,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, @@ -5890,6 +5892,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, @@ -5898,6 +5901,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, @@ -5906,6 +5910,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, diff --git a/prowler/lib/scan/scan.py b/prowler/lib/scan/scan.py index 6af2d26fbc..1d6d8604cc 100644 --- a/prowler/lib/scan/scan.py +++ b/prowler/lib/scan/scan.py @@ -27,6 +27,7 @@ from prowler.lib.scan.exceptions.exceptions import ( from prowler.providers.common.models import Audit_Metadata, ProviderOutputOptions from prowler.providers.common.provider import Provider from prowler.providers.iac.iac_provider import IacProvider +from prowler.providers.image.image_provider import ImageProvider class Scan: @@ -92,10 +93,10 @@ class Scan: except ValueError: raise ScanInvalidStatusError(f"Invalid status provided: {s}.") - # Special setup for IaC provider - override inputs to work with traditional flow - if provider.type == "iac": - # IaC doesn't use traditional Prowler checks, so clear all input parameters - # to avoid validation errors and let it flow through the normal logic + # Special setup for IaC/Image providers - override inputs to work with traditional flow + if provider.type in ("iac", "image"): + # These providers don't use traditional Prowler checks, so clear all input + # parameters to avoid validation errors and let them flow through the normal logic checks = None services = None excluded_checks = None @@ -160,8 +161,8 @@ class Scan: ) # Load checks to execute - if provider.type == "iac": - self._checks_to_execute = ["iac_scan"] # Dummy check name for IaC + if provider.type in ("iac", "image"): + self._checks_to_execute = [f"{provider.type}_scan"] # Dummy check name else: self._checks_to_execute = sorted( load_checks_to_execute( @@ -200,8 +201,8 @@ class Scan: self._number_of_checks_to_execute = len(self._checks_to_execute) # Set up service-based checks tracking - if provider.type == "iac": - service_checks_to_execute = {"iac": set(["iac_scan"])} + if provider.type in ("iac", "image"): + service_checks_to_execute = {provider.type: set([f"{provider.type}_scan"])} else: service_checks_to_execute = get_service_checks_to_execute( self._checks_to_execute @@ -346,6 +347,68 @@ class Scan: self._duration = int((end_time - start_time).total_seconds()) return + # Special handling for Image provider + elif self._provider.type == "image": + if isinstance(self._provider, ImageProvider): + logger.info("Running Image scan with Trivy...") + + total_images = len(self._provider.images) + self._number_of_checks_to_execute = max(total_images, 1) + + for i, (image_name, image_reports) in enumerate( + self._provider.scan_per_image() + ): + # Build resource UID from image name + SHA (all reports share the same SHA) + image_sha = image_reports[0].image_sha if image_reports else "" + resource_uid = ( + f"{image_name}:{image_sha}" if image_sha else image_name + ) + + findings = [] + for report in image_reports: + finding_uid = ( + f"{report.check_metadata.CheckID}" + f"-{image_name}" + f"-{report.resource_id}" + ) + status_enum = ( + Status.FAIL if report.status == "FAIL" else Status.PASS + ) + if report.muted: + status_enum = Status.MUTED + + finding = Finding( + auth_method="Registry", + timestamp=datetime.datetime.now(timezone.utc), + account_uid=getattr(self._provider, "registry", None) + or "image", + account_name="Container Registry", + metadata=report.check_metadata, + uid=finding_uid, + status=status_enum, + status_extended=report.status_extended, + muted=report.muted, + resource_uid=resource_uid, + resource_metadata=report.resource, + resource_name=image_name, + resource_details=report.resource_details, + resource_tags={}, + region=report.region, + compliance={}, + raw=report.resource, + ) + findings.append(finding) + + if self._status: + findings = [f for f in findings if f.status in self._status] + + self._number_of_checks_completed = i + 1 + yield (self.progress, findings) + + end_time = datetime.datetime.now() + self._duration = int((end_time - start_time).total_seconds()) + return + for check_name in checks_to_execute: try: # Recover service from check name diff --git a/prowler/providers/image/exceptions/__init__.py b/prowler/providers/image/exceptions/__init__.py index 95b0b3a110..e69de29bb2 100644 --- a/prowler/providers/image/exceptions/__init__.py +++ b/prowler/providers/image/exceptions/__init__.py @@ -1,19 +0,0 @@ -from prowler.providers.image.exceptions.exceptions import ( - ImageBaseException, - ImageFindingProcessingError, - ImageListFileNotFoundError, - ImageListFileReadError, - ImageNoImagesProvidedError, - ImageScanError, - ImageTrivyBinaryNotFoundError, -) - -__all__ = [ - "ImageBaseException", - "ImageFindingProcessingError", - "ImageListFileNotFoundError", - "ImageListFileReadError", - "ImageNoImagesProvidedError", - "ImageScanError", - "ImageTrivyBinaryNotFoundError", -] diff --git a/prowler/providers/image/exceptions/exceptions.py b/prowler/providers/image/exceptions/exceptions.py index c7b6f42503..387b443ce3 100644 --- a/prowler/providers/image/exceptions/exceptions.py +++ b/prowler/providers/image/exceptions/exceptions.py @@ -1,49 +1,81 @@ from prowler.exceptions.exceptions import ProwlerException -# Exceptions codes from 9000 to 9999 are reserved for Image exceptions +# Exceptions codes from 11000 to 11999 are reserved for Image exceptions class ImageBaseException(ProwlerException): """Base class for Image provider errors.""" IMAGE_ERROR_CODES = { - (9000, "ImageNoImagesProvidedError"): { + (11000, "ImageNoImagesProvidedError"): { "message": "No container images provided for scanning.", "remediation": "Provide at least one image using --image or --image-list-file.", }, - (9001, "ImageListFileNotFoundError"): { + (11001, "ImageListFileNotFoundError"): { "message": "Image list file not found.", "remediation": "Ensure the image list file exists at the specified path.", }, - (9002, "ImageListFileReadError"): { + (11002, "ImageListFileReadError"): { "message": "Error reading image list file.", "remediation": "Check file permissions and format. The file should contain one image per line.", }, - (9003, "ImageFindingProcessingError"): { + (11003, "ImageFindingProcessingError"): { "message": "Error processing image scan finding.", "remediation": "Check the Trivy output format and ensure the finding structure is valid.", }, - (9004, "ImageTrivyBinaryNotFoundError"): { + (11004, "ImageTrivyBinaryNotFoundError"): { "message": "Trivy binary not found.", "remediation": "Install Trivy from https://trivy.dev/latest/getting-started/installation/", }, - (9005, "ImageScanError"): { + (11005, "ImageScanError"): { "message": "Error scanning container image.", "remediation": "Check the image name and ensure it is accessible.", }, - (9006, "ImageDockerLoginError"): { - "message": "Docker login failed for registry authentication.", - "remediation": "Check your registry credentials and ensure the registry is reachable.", + (11006, "ImageInvalidTimeoutError"): { + "message": "Invalid timeout format.", + "remediation": "Use a valid timeout like '5m', '300s', or '1h'.", }, - (9007, "ImageDockerNotFoundError"): { - "message": "Docker binary not found.", - "remediation": "Install Docker to enable private registry authentication via docker login.", + (11007, "ImageInvalidScannerError"): { + "message": "Invalid scanner type.", + "remediation": "Use valid scanners: vuln, secret, misconfig, license.", + }, + (11008, "ImageInvalidSeverityError"): { + "message": "Invalid severity level.", + "remediation": "Use valid severities: CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN.", + }, + (11009, "ImageInvalidNameError"): { + "message": "Invalid container image name.", + "remediation": "Use a valid image reference (e.g., 'alpine:3.18', 'registry.example.com/repo/image:tag').", + }, + (11010, "ImageInvalidConfigScannerError"): { + "message": "Invalid image config scanner type.", + "remediation": "Use valid image config scanners: misconfig, secret.", + }, + (11013, "ImageRegistryAuthError"): { + "message": "Registry authentication failed.", + "remediation": "Check REGISTRY_USERNAME/REGISTRY_PASSWORD or REGISTRY_TOKEN environment variables.", + }, + (11014, "ImageRegistryCatalogError"): { + "message": "Registry does not support catalog listing.", + "remediation": "Use --image or --image-list instead of --registry.", + }, + (11015, "ImageRegistryNetworkError"): { + "message": "Network error communicating with registry.", + "remediation": "Check registry URL and network connectivity.", + }, + (11016, "ImageMaxImagesExceededError"): { + "message": "Discovered images exceed --max-images limit.", + "remediation": "Use --image-filter or --tag-filter to narrow results, or increase --max-images.", + }, + (11017, "ImageInvalidFilterError"): { + "message": "Invalid regex filter pattern.", + "remediation": "Check the regex syntax for --image-filter or --tag-filter.", }, } def __init__(self, code, file=None, original_exception=None, message=None): error_info = self.IMAGE_ERROR_CODES.get((code, self.__class__.__name__)) - if message: - error_info["message"] = message + if error_info and message: + error_info = {**error_info, "message": message} super().__init__( code, source="Image", @@ -58,7 +90,7 @@ class ImageNoImagesProvidedError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9000, file=file, original_exception=original_exception, message=message + 11000, file=file, original_exception=original_exception, message=message ) @@ -67,7 +99,7 @@ class ImageListFileNotFoundError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9001, file=file, original_exception=original_exception, message=message + 11001, file=file, original_exception=original_exception, message=message ) @@ -76,7 +108,7 @@ class ImageListFileReadError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9002, file=file, original_exception=original_exception, message=message + 11002, file=file, original_exception=original_exception, message=message ) @@ -85,7 +117,7 @@ class ImageFindingProcessingError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9003, file=file, original_exception=original_exception, message=message + 11003, file=file, original_exception=original_exception, message=message ) @@ -94,7 +126,7 @@ class ImageTrivyBinaryNotFoundError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9004, file=file, original_exception=original_exception, message=message + 11004, file=file, original_exception=original_exception, message=message ) @@ -103,23 +135,95 @@ class ImageScanError(ImageBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9005, file=file, original_exception=original_exception, message=message + 11005, file=file, original_exception=original_exception, message=message ) -class ImageDockerLoginError(ImageBaseException): - """Exception raised when docker login fails.""" +class ImageInvalidTimeoutError(ImageBaseException): + """Exception raised when an invalid timeout format is provided.""" def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9006, file=file, original_exception=original_exception, message=message + 11006, file=file, original_exception=original_exception, message=message ) -class ImageDockerNotFoundError(ImageBaseException): - """Exception raised when the docker binary is not found.""" +class ImageInvalidScannerError(ImageBaseException): + """Exception raised when an invalid scanner type is provided.""" def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 9007, file=file, original_exception=original_exception, message=message + 11007, file=file, original_exception=original_exception, message=message + ) + + +class ImageInvalidSeverityError(ImageBaseException): + """Exception raised when an invalid severity level is provided.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11008, file=file, original_exception=original_exception, message=message + ) + + +class ImageInvalidNameError(ImageBaseException): + """Exception raised when an invalid container image name is provided.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11009, file=file, original_exception=original_exception, message=message + ) + + +class ImageInvalidConfigScannerError(ImageBaseException): + """Exception raised when an invalid image config scanner type is provided.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11010, file=file, original_exception=original_exception, message=message + ) + + +class ImageRegistryAuthError(ImageBaseException): + """Exception raised when registry authentication fails.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11013, file=file, original_exception=original_exception, message=message + ) + + +class ImageRegistryCatalogError(ImageBaseException): + """Exception raised when registry does not support catalog listing.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11014, file=file, original_exception=original_exception, message=message + ) + + +class ImageRegistryNetworkError(ImageBaseException): + """Exception raised when a network error occurs communicating with a registry.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11015, file=file, original_exception=original_exception, message=message + ) + + +class ImageMaxImagesExceededError(ImageBaseException): + """Exception raised when discovered images exceed --max-images limit.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11016, file=file, original_exception=original_exception, message=message + ) + + +class ImageInvalidFilterError(ImageBaseException): + """Exception raised when an invalid regex filter pattern is provided.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 11017, file=file, original_exception=original_exception, message=message ) diff --git a/prowler/providers/image/image_provider.py b/prowler/providers/image/image_provider.py index 05dcf9c5c6..be16580ba1 100644 --- a/prowler/providers/image/image_provider.py +++ b/prowler/providers/image/image_provider.py @@ -2,9 +2,10 @@ from __future__ import annotations import json import os +import re import subprocess import sys -from typing import Generator, List +from typing import Generator from alive_progress import alive_bar from colorama import Fore, Style @@ -19,15 +20,27 @@ from prowler.lib.utils.utils import print_boxes from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider from prowler.providers.image.exceptions.exceptions import ( - ImageDockerLoginError, - ImageDockerNotFoundError, ImageFindingProcessingError, + ImageInvalidConfigScannerError, + ImageInvalidFilterError, + ImageInvalidNameError, + ImageInvalidScannerError, + ImageInvalidSeverityError, + ImageInvalidTimeoutError, ImageListFileNotFoundError, ImageListFileReadError, + ImageMaxImagesExceededError, ImageNoImagesProvidedError, ImageScanError, ImageTrivyBinaryNotFoundError, ) +from prowler.providers.image.lib.arguments.arguments import ( + IMAGE_CONFIG_SCANNERS_CHOICES, + SCANNERS_CHOICES, + SEVERITY_CHOICES, +) +from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter +from prowler.providers.image.lib.registry.factory import create_registry_adapter class ImageProvider(Provider): @@ -39,28 +52,43 @@ class ImageProvider(Provider): """ _type: str = "image" + FINDING_BATCH_SIZE: int = 100 + MAX_IMAGE_LIST_LINES: int = 10_000 + MAX_IMAGE_NAME_LENGTH: int = 500 + _IMAGE_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9.\-_/:@]+$") + _SHELL_METACHARACTERS = frozenset(";|&$`\n\r") audit_metadata: Audit_Metadata def __init__( self, images: list[str] | None = None, - image_list_file: str = None, + image_list_file: str | None = None, scanners: list[str] | None = None, + image_config_scanners: list[str] | None = None, trivy_severity: list[str] | None = None, ignore_unfixed: bool = False, timeout: str = "5m", - config_path: str = None, - config_content: dict = None, + config_path: str | None = None, + config_content: dict | None = None, fixer_config: dict | None = None, - registry_username: str = None, - registry_password: str = None, - registry_token: str = None, + registry_username: str | None = None, + registry_password: str | None = None, + registry_token: str | None = None, + registry: str | None = None, + image_filter: str | None = None, + tag_filter: str | None = None, + max_images: int = 0, + registry_insecure: bool = False, + registry_list_images: bool = False, ): logger.info("Instantiating Image Provider...") self.images = images if images is not None else [] self.image_list_file = image_list_file self.scanners = scanners if scanners is not None else ["vuln", "secret"] + self.image_config_scanners = ( + image_config_scanners if image_config_scanners is not None else [] + ) self.trivy_severity = trivy_severity if trivy_severity is not None else [] self.ignore_unfixed = ignore_unfixed self.timeout = timeout @@ -68,12 +96,16 @@ class ImageProvider(Provider): self.audited_account = "image-scan" self._session = None self._identity = "prowler" + self._listing_only = False # Registry authentication (follows IaC pattern: explicit params, env vars internal) - self.registry_username = registry_username or os.environ.get("TRIVY_USERNAME") - self.registry_password = registry_password or os.environ.get("TRIVY_PASSWORD") - self.registry_token = registry_token or os.environ.get("TRIVY_REGISTRY_TOKEN") - self._logged_in_registries: set = set() + self.registry_username = registry_username or os.environ.get( + "REGISTRY_USERNAME" + ) + self.registry_password = registry_password or os.environ.get( + "REGISTRY_PASSWORD" + ) + self.registry_token = registry_token or os.environ.get("REGISTRY_TOKEN") if self.registry_username and self.registry_password: self._auth_method = "Docker login" @@ -84,10 +116,49 @@ class ImageProvider(Provider): else: self._auth_method = "No auth" + # Registry scan mode + self.registry = registry + self.image_filter = image_filter + self.tag_filter = tag_filter + self.max_images = max_images + self.registry_insecure = registry_insecure + self.registry_list_images = registry_list_images + + # Compile regex filters + self._image_filter_re = None + self._tag_filter_re = None + if self.image_filter: + try: + self._image_filter_re = re.compile(self.image_filter) + except re.error as exc: + raise ImageInvalidFilterError( + file=__file__, + message=f"Invalid --image-filter regex '{self.image_filter}': {exc}", + ) + if self.tag_filter: + try: + self._tag_filter_re = re.compile(self.tag_filter) + except re.error as exc: + raise ImageInvalidFilterError( + file=__file__, + message=f"Invalid --tag-filter regex '{self.tag_filter}': {exc}", + ) + + self._validate_inputs() + # Load images from file if provided if image_list_file: self._load_images_from_file(image_list_file) + # Registry scan mode: enumerate images from registry + if self.registry: + self._enumerate_registry() + if self._listing_only: + return + + for image in self.images: + self._validate_image_name(image) + if not self.images: raise ImageNoImagesProvidedError( file=__file__, @@ -124,17 +195,32 @@ class ImageProvider(Provider): def _load_images_from_file(self, file_path: str) -> None: """Load image names from a file (one per line).""" try: + line_count = 0 with open(file_path, "r") as f: for line in f: + line_count += 1 + if line_count > self.MAX_IMAGE_LIST_LINES: + raise ImageListFileReadError( + file=file_path, + message=f"Image list file exceeds maximum of {self.MAX_IMAGE_LIST_LINES} lines.", + ) line = line.strip() - if line and not line.startswith("#"): - self.images.append(line) + if not line or line.startswith("#"): + continue + if len(line) > self.MAX_IMAGE_NAME_LENGTH: + logger.warning( + f"Skipping image name exceeding {self.MAX_IMAGE_NAME_LENGTH} chars at line {line_count} in {file_path}" + ) + continue + self.images.append(line) logger.info(f"Loaded {len(self.images)} images from {file_path}") except FileNotFoundError: raise ImageListFileNotFoundError( file=file_path, message=f"Image list file not found: {file_path}", ) + except (ImageListFileReadError, ImageListFileNotFoundError): + raise except Exception as error: raise ImageListFileReadError( file=file_path, @@ -142,6 +228,61 @@ class ImageProvider(Provider): message=f"Error reading image list file: {error}", ) + def _validate_inputs(self) -> None: + """Validate timeout, scanners, and severity inputs.""" + if not re.fullmatch(r"\d+[smh]", self.timeout): + raise ImageInvalidTimeoutError( + file=__file__, + message=f"Invalid timeout format: '{self.timeout}'. Expected pattern like '5m', '300s', or '1h'.", + ) + + for scanner in self.scanners: + if scanner not in SCANNERS_CHOICES: + raise ImageInvalidScannerError( + file=__file__, + message=f"Invalid scanner: '{scanner}'. Valid options: {', '.join(SCANNERS_CHOICES)}.", + ) + + for config_scanner in self.image_config_scanners: + if config_scanner not in IMAGE_CONFIG_SCANNERS_CHOICES: + raise ImageInvalidConfigScannerError( + file=__file__, + message=f"Invalid image config scanner: '{config_scanner}'. Valid options: {', '.join(IMAGE_CONFIG_SCANNERS_CHOICES)}.", + ) + + for severity in self.trivy_severity: + if severity not in SEVERITY_CHOICES: + raise ImageInvalidSeverityError( + file=__file__, + message=f"Invalid severity: '{severity}'. Valid options: {', '.join(SEVERITY_CHOICES)}.", + ) + + def _validate_image_name(self, name: str) -> None: + """Validate a container image name for safety and correctness.""" + if not name: + raise ImageInvalidNameError( + file=__file__, + message="Image name must not be empty.", + ) + + if len(name) > self.MAX_IMAGE_NAME_LENGTH: + raise ImageInvalidNameError( + file=__file__, + message=f"Image name exceeds maximum length of {self.MAX_IMAGE_NAME_LENGTH} characters: '{name[:50]}...'", + ) + + if any(c in self._SHELL_METACHARACTERS for c in name): + raise ImageInvalidNameError( + file=__file__, + message=f"Image name contains invalid characters: '{name}'", + ) + + if not self._IMAGE_NAME_PATTERN.fullmatch(name): + raise ImageInvalidNameError( + file=__file__, + message=f"Image name does not match valid OCI reference format: '{name}'", + ) + @property def auth_method(self) -> str: return self._auth_method @@ -181,120 +322,48 @@ class ImageProvider(Provider): return parts[0] return None - def _docker_login(self, registry: str | None) -> None: - """Run docker login --password-stdin for a registry.""" - registry_label = registry or "Docker Hub" - cmd = [ - "docker", - "login", - "--username", - self.registry_username, - "--password-stdin", - ] - if registry: - cmd.append(registry) - - try: - process = subprocess.run( - cmd, - input=self.registry_password, - capture_output=True, - text=True, - ) - if process.returncode != 0: - raise ImageDockerLoginError( - file=__file__, - message=f"docker login failed for {registry_label}: {process.stderr.strip()}", - ) - logger.info(f"Docker login successful for {registry_label}") - self._logged_in_registries.add(registry) - except FileNotFoundError: - raise ImageDockerNotFoundError( - file=__file__, - message="Docker binary not found. Install Docker to use registry authentication.", - ) - - def _docker_pull(self, image: str) -> None: - """Pull an image using docker pull so Trivy can scan it from the local daemon. - - Trivy's remote source cannot authenticate against Docker Hub (and some - other registries) even after docker login. Pulling into the local - Docker daemon first lets Trivy find the image via its "docker" source. - """ - try: - logger.info(f"Pulling image {image} via docker...") - process = subprocess.run( - ["docker", "pull", image], - capture_output=True, - text=True, - ) - if process.returncode != 0: - raise ImageScanError( - file=__file__, - message=f"docker pull failed for {image}: {process.stderr.strip()}", - ) - logger.info(f"Successfully pulled {image}") - except FileNotFoundError: - raise ImageDockerNotFoundError( - file=__file__, - message="Docker binary not found. Install Docker to use registry authentication.", - ) - - def _docker_logout(self, registry: str | None) -> None: - """Run docker logout for a registry. Logs warning on failure, never raises.""" - registry_label = registry or "Docker Hub" - cmd = ["docker", "logout"] - if registry: - cmd.append(registry) - - try: - process = subprocess.run(cmd, capture_output=True, text=True) - if process.returncode != 0: - logger.warning( - f"docker logout failed for {registry_label}: {process.stderr.strip()}" - ) - else: - logger.info(f"Docker logout successful for {registry_label}") - except Exception as error: - logger.warning(f"docker logout failed for {registry_label}: {error}") - def cleanup(self) -> None: - """Logout from all registries that were logged in during the scan.""" - for registry in self._logged_in_registries: - self._docker_logout(registry) - self._logged_in_registries.clear() + """Clean up any resources after scanning.""" def _process_finding( - self, finding: dict, image_name: str, finding_type: str + self, + finding: dict, + image: str, + trivy_target: str, + image_sha: str = "", ) -> CheckReportImage: """ Process a single finding and create a CheckReportImage object. Args: finding: The finding object from Trivy output - image_name: The container image name being scanned - finding_type: The type of finding (Vulnerability, Secret, etc.) + image: The clean container image name (e.g., "alpine:3.18") + trivy_target: The Trivy target string (e.g., "alpine:3.18 (alpine 3.18.0)") + image_sha: Short SHA from Trivy Metadata.ImageID for resource uniqueness Returns: CheckReportImage: The processed check report """ try: - # Determine finding ID based on type + # Determine finding ID and category based on type if "VulnerabilityID" in finding: finding_id = finding["VulnerabilityID"] finding_description = finding.get( "Description", finding.get("Title", "") ) finding_status = "FAIL" + finding_categories = ["vulnerability"] elif "RuleID" in finding: # Secret finding finding_id = finding["RuleID"] finding_description = finding.get("Title", "Secret detected") finding_status = "FAIL" + finding_categories = ["secrets"] else: finding_id = finding.get("ID", "UNKNOWN") finding_description = finding.get("Description", "") finding_status = finding.get("Status", "FAIL") + finding_categories = [] # Build remediation text for vulnerabilities remediation_text = "" @@ -313,7 +382,7 @@ class ImageProvider(Provider): "CheckID": finding_id, "CheckTitle": finding.get("Title", finding_id), "CheckType": ["Container Image Security"], - "ServiceName": finding_type, + "ServiceName": "container-image", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": trivy_severity, @@ -323,7 +392,7 @@ class ImageProvider(Provider): "Risk": finding.get( "Description", "Vulnerability detected in container image" ), - "RelatedUrl": finding.get("PrimaryURL", ""), + "RelatedUrl": "", "Remediation": { "Code": { "NativeIaC": "", @@ -336,7 +405,7 @@ class ImageProvider(Provider): "Url": finding.get("PrimaryURL", ""), }, }, - "Categories": [], + "Categories": finding_categories, "DependsOn": [], "RelatedTo": [], "Notes": "", @@ -346,18 +415,20 @@ class ImageProvider(Provider): metadata = json.dumps(metadata_dict) report = CheckReportImage( - metadata=metadata, finding=finding, image_name=image_name + metadata=metadata, finding=finding, image_name=image ) report.status = finding_status report.status_extended = self._build_status_extended(finding) report.region = self.region + report.image_sha = image_sha + report.resource_details = trivy_target return report except Exception as error: raise ImageFindingProcessingError( file=__file__, original_exception=error, - message=f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}", + message=f"Error processing finding: {error}", ) def _build_status_extended(self, finding: dict) -> str: @@ -385,7 +456,7 @@ class ImageProvider(Provider): " ".join(parts) if parts else finding.get("Description", "Finding detected") ) - def run(self) -> List[CheckReportImage]: + def run(self) -> list[CheckReportImage]: """Execute the container image scan.""" try: reports = [] @@ -395,20 +466,36 @@ class ImageProvider(Provider): finally: self.cleanup() - def run_scan(self) -> Generator[List[CheckReportImage], None, None]: + def scan_per_image( + self, + ) -> Generator[tuple[str, list[CheckReportImage]], None, None]: + """Scan images one by one, yielding (image_name, findings) per image. + + Unlike run() which returns all findings at once, this method yields + after each image completes, enabling progress tracking. + """ + try: + for image in self.images: + try: + image_findings = [] + for batch in self._scan_single_image(image): + image_findings.extend(batch) + yield (image, image_findings) + except (ImageScanError, ImageTrivyBinaryNotFoundError): + raise + except Exception as error: + logger.error(f"Error scanning image {image}: {error}") + continue + finally: + self.cleanup() + + def run_scan(self) -> Generator[list[CheckReportImage], None, None]: """ Run Trivy scan on all configured images. Yields: - List[CheckReportImage]: Batches of findings + list[CheckReportImage]: Batches of findings """ - if self.registry_username and self.registry_password: - registries = {self._extract_registry(img) for img in self.images} - for registry in registries: - self._docker_login(registry) - for image in self.images: - self._docker_pull(image) - for image in self.images: try: yield from self._scan_single_image(image) @@ -420,7 +507,7 @@ class ImageProvider(Provider): def _scan_single_image( self, image: str - ) -> Generator[List[CheckReportImage], None, None]: + ) -> Generator[list[CheckReportImage], None, None]: """ Scan a single container image with Trivy. @@ -428,7 +515,7 @@ class ImageProvider(Provider): image: The container image name/tag to scan Yields: - List[CheckReportImage]: Batches of findings + list[CheckReportImage]: Batches of findings """ try: logger.info(f"Scanning container image: {image}") @@ -445,6 +532,11 @@ class ImageProvider(Provider): self.timeout, ] + if self.image_config_scanners: + trivy_command.extend( + ["--image-config-scanners", ",".join(self.image_config_scanners)] + ) + if self.trivy_severity: trivy_command.extend(["--severity", ",".join(self.trivy_severity)]) @@ -463,9 +555,10 @@ class ImageProvider(Provider): # Check for Trivy failure if process.returncode != 0: error_msg = self._extract_trivy_errors(process.stderr) + categorized_msg = self._categorize_trivy_error(error_msg) raise ImageScanError( file=__file__, - message=f"Trivy scan failed for {image}: {error_msg}", + message=f"Trivy scan failed for {image}: {categorized_msg}", ) # Parse JSON output @@ -477,6 +570,19 @@ class ImageProvider(Provider): logger.info(f"No findings for image: {image}") return + # Extract image digest for resource uniqueness + trivy_metadata = output.get("Metadata", {}) + image_id = trivy_metadata.get("ImageID", "") + if not image_id: + repo_digests = trivy_metadata.get("RepoDigests", []) + if repo_digests: + image_id = ( + repo_digests[0].split("@")[-1] + if "@" in repo_digests[0] + else "" + ) + short_sha = image_id.replace("sha256:", "")[:12] if image_id else "" + except json.JSONDecodeError as error: logger.error(f"Failed to parse Trivy output for {image}: {error}") logger.debug(f"Trivy stdout: {process.stdout[:500]}") @@ -484,35 +590,37 @@ class ImageProvider(Provider): # Process findings in batches batch = [] - batch_size = 100 for result in results: target = result.get("Target", image) - result_type = result.get("Type", "unknown") # Process Vulnerabilities for vuln in result.get("Vulnerabilities", []): - report = self._process_finding(vuln, target, result_type) + report = self._process_finding( + vuln, image, target, image_sha=short_sha + ) batch.append(report) - if len(batch) >= batch_size: + if len(batch) >= self.FINDING_BATCH_SIZE: yield batch batch = [] # Process Secrets for secret in result.get("Secrets", []): - report = self._process_finding(secret, target, "secret") + report = self._process_finding( + secret, image, target, image_sha=short_sha + ) batch.append(report) - if len(batch) >= batch_size: + if len(batch) >= self.FINDING_BATCH_SIZE: yield batch batch = [] # Process Misconfigurations (from Dockerfile) for misconfig in result.get("Misconfigurations", []): report = self._process_finding( - misconfig, target, "misconfiguration" + misconfig, image, target, image_sha=short_sha ) batch.append(report) - if len(batch) >= batch_size: + if len(batch) >= self.FINDING_BATCH_SIZE: yield batch batch = [] @@ -532,15 +640,12 @@ class ImageProvider(Provider): logger.error(f"Error scanning image {image}: {error}") def _build_trivy_env(self) -> dict: - """Build environment variables for Trivy, injecting registry credentials. - - When username+password are provided, we rely on docker login (which stores - credentials in Docker's credential store). Setting TRIVY_USERNAME/TRIVY_PASSWORD - would cause Trivy to attempt its own token-exchange auth, which fails for - Docker Hub and other registries that require docker login. - """ + """Build environment variables for Trivy, injecting registry credentials.""" env = dict(os.environ) - if self.registry_token: + if self.registry_username and self.registry_password: + env["TRIVY_USERNAME"] = self.registry_username + env["TRIVY_PASSWORD"] = self.registry_password + elif self.registry_token: env["TRIVY_REGISTRY_TOKEN"] = self.registry_token return env @@ -620,6 +725,121 @@ class ImageProvider(Provider): return line.strip()[:500] return "Unknown error" + @staticmethod + def _categorize_trivy_error(error_msg: str) -> str: + """Categorize a Trivy error message to provide actionable guidance.""" + lower = error_msg.lower() + + if any(kw in lower for kw in ("401", "403", "unauthorized", "denied")): + return f"Auth failure — check `docker login`: {error_msg}" + if any(kw in lower for kw in ("404", "manifest unknown", "not found")): + return f"Image not found — check name/tag/registry: {error_msg}" + if any(kw in lower for kw in ("429", "rate limit", "too many requests")): + return f"Rate limited — wait or authenticate: {error_msg}" + if any(kw in lower for kw in ("timeout", "connection refused", "no such host")): + return f"Network issue — check connectivity: {error_msg}" + + return error_msg + + def _enumerate_registry(self) -> None: + """Enumerate images from a registry using the appropriate adapter.""" + verify_ssl = not self.registry_insecure + adapter = create_registry_adapter( + registry_url=self.registry, + username=self.registry_username, + password=self.registry_password, + token=self.registry_token, + verify_ssl=verify_ssl, + ) + + repositories = adapter.list_repositories() + logger.info( + f"Discovered {len(repositories)} repositories from registry {self.registry}" + ) + + # Apply image filter + if self._image_filter_re: + repositories = [r for r in repositories if self._image_filter_re.search(r)] + logger.info( + f"{len(repositories)} repositories match --image-filter '{self.image_filter}'" + ) + + if not repositories: + logger.warning( + f"No repositories found in registry {self.registry} (after filtering)" + ) + return + + # Determine if this is a Docker Hub adapter (for image reference format) + is_dockerhub = isinstance(adapter, DockerHubAdapter) + + discovered_images = [] + repos_tags: dict[str, list[str]] = {} + for repo in repositories: + tags = adapter.list_tags(repo) + + # Apply tag filter + if self._tag_filter_re: + tags = [t for t in tags if self._tag_filter_re.search(t)] + + if tags: + repos_tags[repo] = tags + + for tag in tags: + if is_dockerhub: + # Docker Hub images don't need a host prefix + image_ref = f"{repo}:{tag}" + else: + # OCI registries need the full host/repo:tag reference + registry_host = self.registry.rstrip("/") + for prefix in ("https://", "http://"): + if registry_host.startswith(prefix): + registry_host = registry_host[len(prefix) :] + break + image_ref = f"{registry_host}/{repo}:{tag}" + discovered_images.append(image_ref) + + # Registry list mode: print listing and return early + if self.registry_list_images: + self._print_registry_listing(repos_tags, len(discovered_images)) + self._listing_only = True + return + + # Check max-images limit + if self.max_images and len(discovered_images) > self.max_images: + raise ImageMaxImagesExceededError( + file=__file__, + message=f"Discovered {len(discovered_images)} images, exceeding --max-images {self.max_images}. Use --image-filter or --tag-filter to narrow results.", + ) + + # Deduplicate with explicit images + existing = set(self.images) + for img in discovered_images: + if img not in existing: + self.images.append(img) + existing.add(img) + + logger.info( + f"Discovered {len(discovered_images)} images from registry {self.registry} " + f"({len(repositories)} repositories). Total images to scan: {len(self.images)}" + ) + + def _print_registry_listing( + self, repos_tags: dict[str, list[str]], total_images: int + ) -> None: + """Print a structured listing of registry repositories and tags.""" + num_repos = len(repos_tags) + print( + f"\n{Style.BRIGHT}Registry:{Style.RESET_ALL} " + f"{Fore.CYAN}{self.registry}{Style.RESET_ALL} " + f"({num_repos} {'repository' if num_repos == 1 else 'repositories'}, " + f"{total_images} {'image' if total_images == 1 else 'images'})\n" + ) + for repo, tags in repos_tags.items(): + print(f" {Fore.YELLOW}{repo}{Style.RESET_ALL} " f"({len(tags)} tags)") + print(f" {', '.join(tags)}") + print() + def print_credentials(self) -> None: """Print scan configuration.""" report_title = f"{Style.BRIGHT}Scanning container images:{Style.RESET_ALL}" @@ -637,6 +857,11 @@ class ImageProvider(Provider): f"Scanners: {Fore.YELLOW}{', '.join(self.scanners)}{Style.RESET_ALL}" ) + if self.image_config_scanners: + report_lines.append( + f"Image config scanners: {Fore.YELLOW}{', '.join(self.image_config_scanners)}{Style.RESET_ALL}" + ) + if self.trivy_severity: report_lines.append( f"Severity filter: {Fore.YELLOW}{', '.join(self.trivy_severity)}{Style.RESET_ALL}" @@ -651,16 +876,29 @@ class ImageProvider(Provider): f"Authentication method: {Fore.YELLOW}{self.auth_method}{Style.RESET_ALL}" ) + if self.registry: + report_lines.append( + f"Registry: {Fore.YELLOW}{self.registry}{Style.RESET_ALL}" + ) + if self.image_filter: + report_lines.append( + f"Image filter: {Fore.YELLOW}{self.image_filter}{Style.RESET_ALL}" + ) + if self.tag_filter: + report_lines.append( + f"Tag filter: {Fore.YELLOW}{self.tag_filter}{Style.RESET_ALL}" + ) + print_boxes(report_lines, report_title) @staticmethod def test_connection( - image: str = None, + image: str | None = None, raise_on_exception: bool = True, - provider_id: str = None, - registry_username: str = None, - registry_password: str = None, - registry_token: str = None, + provider_id: str | None = None, + registry_username: str | None = None, + registry_password: str | None = None, + registry_token: str | None = None, ) -> "Connection": """ Test connection to container registry by attempting to inspect an image. @@ -676,8 +914,6 @@ class ImageProvider(Provider): Returns: Connection: Connection object with success status """ - docker_logged_in = False - registry = None try: if provider_id and not image: image = provider_id @@ -688,49 +924,8 @@ class ImageProvider(Provider): # Build env with registry credentials env = dict(os.environ) if registry_username and registry_password: - # Docker login for reliable private registry auth - registry = ImageProvider._extract_registry(image) - registry_label = registry or "Docker Hub" - login_cmd = [ - "docker", - "login", - "--username", - registry_username, - "--password-stdin", - ] - if registry: - login_cmd.append(registry) - try: - login_result = subprocess.run( - login_cmd, - input=registry_password, - capture_output=True, - text=True, - ) - if login_result.returncode == 0: - docker_logged_in = True - logger.info( - f"Docker login successful for {registry_label} (test_connection)" - ) - # Pull the image so Trivy can scan from the local daemon - pull_result = subprocess.run( - ["docker", "pull", image], - capture_output=True, - text=True, - ) - if pull_result.returncode != 0: - return Connection( - is_connected=False, - error=f"docker pull failed for {image}: {pull_result.stderr.strip()}", - ) - else: - logger.warning( - f"Docker login failed for {registry_label}: {login_result.stderr.strip()}" - ) - except FileNotFoundError: - logger.warning( - "Docker binary not found, skipping docker login for test_connection" - ) + env["TRIVY_USERNAME"] = registry_username + env["TRIVY_PASSWORD"] = registry_password elif registry_token: env["TRIVY_REGISTRY_TOKEN"] = registry_token @@ -786,12 +981,3 @@ class ImageProvider(Provider): is_connected=False, error=f"Unexpected error: {str(error)}", ) - finally: - if docker_logged_in: - logout_cmd = ["docker", "logout"] - if registry: - logout_cmd.append(registry) - try: - subprocess.run(logout_cmd, capture_output=True, text=True) - except Exception: - pass diff --git a/prowler/providers/image/lib/arguments/arguments.py b/prowler/providers/image/lib/arguments/arguments.py index e27efac97e..3dfe9cf92b 100644 --- a/prowler/providers/image/lib/arguments/arguments.py +++ b/prowler/providers/image/lib/arguments/arguments.py @@ -5,6 +5,11 @@ SCANNERS_CHOICES = [ "license", ] +IMAGE_CONFIG_SCANNERS_CHOICES = [ + "misconfig", + "secret", +] + SEVERITY_CHOICES = [ "CRITICAL", "HIGH", @@ -50,6 +55,15 @@ def init_parser(self): help="Trivy scanners to use. Default: vuln, secret. Available: vuln, secret, misconfig, license", ) + scan_config_group.add_argument( + "--image-config-scanners", + dest="image_config_scanners", + nargs="+", + default=[], + choices=IMAGE_CONFIG_SCANNERS_CHOICES, + help="Trivy image config scanners (scans Dockerfile-level metadata). Available: misconfig, secret", + ) + scan_config_group.add_argument( "--trivy-severity", dest="trivy_severity", @@ -74,16 +88,96 @@ def init_parser(self): help="Trivy scan timeout. Default: 5m. Examples: 10m, 1h", ) + # Registry Scan Mode + registry_group = image_parser.add_argument_group("Registry Scan Mode") + registry_group.add_argument( + "--registry", + dest="registry", + default=None, + help="Registry URL to enumerate and scan all images. Examples: myregistry.io, docker.io/myorg, 123456789.dkr.ecr.us-east-1.amazonaws.com", + ) + registry_group.add_argument( + "--image-filter", + dest="image_filter", + default=None, + help="Regex to filter repository names during registry enumeration (re.search). Example: '^prod/.*'", + ) + registry_group.add_argument( + "--tag-filter", + dest="tag_filter", + default=None, + help=r"Regex to filter tags during registry enumeration (re.search). Example: '^(latest|v\d+\.\d+\.\d+)$'", + ) + registry_group.add_argument( + "--max-images", + dest="max_images", + type=int, + default=0, + help="Maximum number of images to scan from registry. 0 = unlimited. Aborts if exceeded.", + ) + registry_group.add_argument( + "--registry-insecure", + dest="registry_insecure", + action="store_true", + default=False, + help="Skip TLS verification for registry connections (for self-signed certificates).", + ) + registry_group.add_argument( + "--registry-list", + dest="registry_list_images", + action="store_true", + default=False, + help="List all repositories and tags from the registry, then exit without scanning. Useful for discovering available images before building --image-filter or --tag-filter.", + ) + def validate_arguments(arguments): """Validate Image provider arguments.""" images = getattr(arguments, "images", []) image_list_file = getattr(arguments, "image_list_file", None) + registry = getattr(arguments, "registry", None) + image_filter = getattr(arguments, "image_filter", None) + tag_filter = getattr(arguments, "tag_filter", None) + max_images = getattr(arguments, "max_images", 0) + registry_insecure = getattr(arguments, "registry_insecure", False) + registry_list_images = getattr(arguments, "registry_list_images", False) - if not images and not image_list_file: + if registry_list_images and not registry: + return (False, "--registry-list requires --registry.") + + if not images and not image_list_file and not registry: return ( False, - "At least one image must be specified using --image (-I) or --image-list.", + "At least one image source must be specified using --image (-I), --image-list, or --registry.", ) + # Registry-only flags require --registry + if not registry: + if image_filter: + return (False, "--image-filter requires --registry.") + if tag_filter: + return (False, "--tag-filter requires --registry.") + if max_images: + return (False, "--max-images requires --registry.") + if registry_insecure: + return (False, "--registry-insecure requires --registry.") + + # Docker Hub namespace validation + if registry: + url = registry.rstrip("/") + for prefix in ("https://", "http://"): + if url.startswith(prefix): + url = url[len(prefix) :] + break + stripped = url + for prefix in ("registry-1.docker.io", "docker.io"): + if stripped.startswith(prefix): + stripped = stripped[len(prefix) :].lstrip("/") + if not stripped: + return ( + False, + "Docker Hub requires a namespace. Use --registry docker.io/{org_or_user}.", + ) + break + return (True, "") diff --git a/prowler/providers/image/lib/registry/__init__.py b/prowler/providers/image/lib/registry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/image/lib/registry/base.py b/prowler/providers/image/lib/registry/base.py new file mode 100644 index 0000000000..133ef9d4e0 --- /dev/null +++ b/prowler/providers/image/lib/registry/base.py @@ -0,0 +1,141 @@ +"""Registry adapter abstract base class.""" + +from __future__ import annotations + +import re +import time +from abc import ABC, abstractmethod + +import requests + +from prowler.config.config import prowler_version +from prowler.lib.logger import logger +from prowler.providers.image.exceptions.exceptions import ImageRegistryNetworkError + +_MAX_RETRIES = 3 +_BACKOFF_BASE = 1 +_USER_AGENT = f"Prowler/{prowler_version} (registry-adapter)" + + +class RegistryAdapter(ABC): + """Abstract base class for registry adapters.""" + + def __init__( + self, + registry_url: str, + username: str | None = None, + password: str | None = None, + token: str | None = None, + verify_ssl: bool = True, + ) -> None: + self.registry_url = registry_url + self.username = username + self._password = password + self._token = token + self.verify_ssl = verify_ssl + + @property + def password(self) -> str | None: + return self._password + + @property + def token(self) -> str | None: + return self._token + + def __getstate__(self) -> dict: + state = self.__dict__.copy() + state["_password"] = "***" if state.get("_password") else None + state["_token"] = "***" if state.get("_token") else None + return state + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"registry_url={self.registry_url!r}, " + f"username={self.username!r}, " + f"password={'' if self._password else None}, " + f"token={'' if self._token else None})" + ) + + @abstractmethod + def list_repositories(self) -> list[str]: + """Enumerate all repository names in the registry.""" + ... + + @abstractmethod + def list_tags(self, repository: str) -> list[str]: + """Enumerate all tags for a repository.""" + ... + + def _request_with_retry(self, method: str, url: str, **kwargs) -> requests.Response: + context_label = kwargs.pop("context_label", None) or self.registry_url + kwargs.setdefault("timeout", 30) + kwargs.setdefault("verify", self.verify_ssl) + headers = kwargs.get("headers", {}) + headers.setdefault("User-Agent", _USER_AGENT) + kwargs["headers"] = headers + last_exception = None + last_status = None + last_body = None + for attempt in range(1, _MAX_RETRIES + 1): + try: + resp = requests.request(method, url, **kwargs) + if resp.status_code == 429: + last_status = 429 + wait = _BACKOFF_BASE * (2 ** (attempt - 1)) + logger.warning( + f"Rate limited by {context_label}, retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES})" + ) + time.sleep(wait) + continue + if resp.status_code >= 500: + last_status = resp.status_code + last_body = (resp.text or "")[:500] + wait = _BACKOFF_BASE * (2 ** (attempt - 1)) + logger.warning( + f"Server error from {context_label} (HTTP {resp.status_code}), " + f"retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES}): {last_body}" + ) + time.sleep(wait) + continue + return resp + except requests.exceptions.ConnectionError as exc: + last_exception = exc + if attempt < _MAX_RETRIES: + wait = _BACKOFF_BASE * (2 ** (attempt - 1)) + logger.warning( + f"Connection error to {context_label}, retrying in {wait}s (attempt {attempt}/{_MAX_RETRIES})" + ) + time.sleep(wait) + continue + except requests.exceptions.Timeout as exc: + raise ImageRegistryNetworkError( + file=__file__, + message=f"Connection timed out to {context_label}.", + original_exception=exc, + ) + if last_status == 429: + raise ImageRegistryNetworkError( + file=__file__, + message=f"Rate limited by {context_label} after {_MAX_RETRIES} attempts.", + ) + if last_status is not None and last_status >= 500: + raise ImageRegistryNetworkError( + file=__file__, + message=f"Server error from {context_label} (HTTP {last_status}) after {_MAX_RETRIES} attempts: {last_body}", + ) + raise ImageRegistryNetworkError( + file=__file__, + message=f"Failed to connect to {context_label} after {_MAX_RETRIES} attempts.", + original_exception=last_exception, + ) + + @staticmethod + def _next_page_url(resp: requests.Response) -> str | None: + link_header = resp.headers.get("Link", "") + if not link_header: + return None + match = re.search(r'<([^>]+)>;\s*rel="next"', link_header) + if match: + return match.group(1) + return None diff --git a/prowler/providers/image/lib/registry/dockerhub_adapter.py b/prowler/providers/image/lib/registry/dockerhub_adapter.py new file mode 100644 index 0000000000..949de2d9cb --- /dev/null +++ b/prowler/providers/image/lib/registry/dockerhub_adapter.py @@ -0,0 +1,221 @@ +"""Docker Hub registry adapter.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from prowler.lib.logger import logger +from prowler.providers.image.exceptions.exceptions import ( + ImageRegistryAuthError, + ImageRegistryCatalogError, + ImageRegistryNetworkError, +) +from prowler.providers.image.lib.registry.base import RegistryAdapter + +if TYPE_CHECKING: + import requests + +_HUB_API = "https://hub.docker.com" +_REGISTRY_HOST = "https://registry-1.docker.io" +_AUTH_URL = "https://auth.docker.io/token" + + +class DockerHubAdapter(RegistryAdapter): + """Adapter for Docker Hub using the Hub REST API + OCI tag listing.""" + + def __init__( + self, + registry_url: str, + username: str | None = None, + password: str | None = None, + token: str | None = None, + verify_ssl: bool = True, + ) -> None: + if not verify_ssl: + logger.warning( + "Docker Hub always uses TLS verification; --registry-insecure is ignored for Docker Hub registries." + ) + super().__init__(registry_url, username, password, token, verify_ssl=True) + self.namespace = self._extract_namespace(registry_url) + self._hub_jwt: str | None = None + self._registry_tokens: dict[str, str] = {} + + @staticmethod + def _extract_namespace(registry_url: str) -> str: + url = registry_url.rstrip("/") + for prefix in ( + "https://registry-1.docker.io", + "http://registry-1.docker.io", + "https://docker.io", + "http://docker.io", + "registry-1.docker.io", + "docker.io", + "https://", + "http://", + ): + if url.startswith(prefix): + url = url[len(prefix) :] + break + url = url.lstrip("/") + parts = url.split("/") + namespace = parts[0] if parts and parts[0] else "" + return namespace + + def list_repositories(self) -> list[str]: + if not self.namespace: + raise ImageRegistryCatalogError( + file=__file__, + message="Docker Hub requires a namespace. Use --registry docker.io/{org_or_user}.", + ) + self._hub_login() + repositories: list[str] = [] + if self._hub_jwt: + url = f"{_HUB_API}/v2/namespaces/{self.namespace}/repositories" + else: + url = f"{_HUB_API}/v2/repositories/{self.namespace}/" + params: dict = {"page_size": 100} + while url: + resp = self._hub_request("GET", url, params=params) + self._check_hub_response(resp, "repository listing") + data = resp.json() + for repo in data.get("results", []): + name = repo.get("name", "") + if name: + repositories.append(f"{self.namespace}/{name}") + url = data.get("next") + params = {} + return repositories + + def list_tags(self, repository: str) -> list[str]: + token = self._get_registry_token(repository) + tags: list[str] = [] + url = f"{_REGISTRY_HOST}/v2/{repository}/tags/list" + params: dict = {"n": 100} + while url: + resp = self._registry_request("GET", url, token, params=params) + if resp.status_code in (401, 403): + raise ImageRegistryAuthError( + file=__file__, + message=f"Authentication failed for tag listing of {repository} on Docker Hub. Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + if resp.status_code != 200: + logger.warning( + f"Failed to list tags for {repository} (HTTP {resp.status_code}): {resp.text[:200]}" + ) + break + data = resp.json() + tags.extend(data.get("tags", []) or []) + url = self._next_tag_page_url(resp) + params = {} + return tags + + def _hub_login(self) -> None: + if self._hub_jwt: + return + if not self.username or not self.password: + return + logger.debug(f"Docker Hub login attempt for username: {self.username!r}") + resp = self._request_with_retry( + "POST", + f"{_HUB_API}/v2/users/login", + json={"username": self.username, "password": self.password}, + context_label="Docker Hub", + ) + if resp.status_code != 200: + body_preview = resp.text[:200] if resp.text else "(empty body)" + raise ImageRegistryAuthError( + file=__file__, + message=( + f"Docker Hub login failed (HTTP {resp.status_code}). " + f"Check REGISTRY_USERNAME and REGISTRY_PASSWORD. " + f"Response: {body_preview}" + ), + ) + self._hub_jwt = resp.json().get("token") + if not self._hub_jwt: + raise ImageRegistryAuthError( + file=__file__, + message="Docker Hub login returned an empty JWT token. Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + + def _get_registry_token(self, repository: str) -> str: + if repository in self._registry_tokens: + return self._registry_tokens[repository] + params = { + "service": "registry.docker.io", + "scope": f"repository:{repository}:pull", + } + auth = None + if self.username and self.password: + auth = (self.username, self.password) + resp = self._request_with_retry( + "GET", + _AUTH_URL, + params=params, + auth=auth, + context_label="Docker Hub", + ) + if resp.status_code != 200: + raise ImageRegistryAuthError( + file=__file__, + message=f"Failed to obtain Docker Hub registry token for {repository} (HTTP {resp.status_code}). Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + token = resp.json().get("token", "") + if not token: + raise ImageRegistryAuthError( + file=__file__, + message=f"Docker Hub registry token endpoint returned an empty token for {repository}. Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + self._registry_tokens[repository] = token + return token + + def _hub_request(self, method: str, url: str, **kwargs) -> requests.Response: + headers = kwargs.pop("headers", {}) + if self._hub_jwt: + headers["Authorization"] = f"Bearer {self._hub_jwt}" + kwargs["headers"] = headers + return self._request_with_retry( + method, url, context_label="Docker Hub", **kwargs + ) + + def _registry_request( + self, method: str, url: str, token: str, **kwargs + ) -> requests.Response: + headers = kwargs.pop("headers", {}) + headers["Authorization"] = f"Bearer {token}" + kwargs["headers"] = headers + return self._request_with_retry( + method, url, context_label="Docker Hub", **kwargs + ) + + def _check_hub_response(self, resp: requests.Response, context: str) -> None: + if resp.status_code == 200: + return + if resp.status_code in (401, 403): + raise ImageRegistryAuthError( + file=__file__, + message=f"Authentication failed for {context} on Docker Hub (HTTP {resp.status_code}). Check REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables.", + ) + if resp.status_code == 404: + raise ImageRegistryCatalogError( + file=__file__, + message=f"Namespace '{self.namespace}' not found on Docker Hub. Check the namespace in --registry docker.io/{{namespace}}.", + ) + raise ImageRegistryNetworkError( + file=__file__, + message=f"Unexpected error during {context} on Docker Hub (HTTP {resp.status_code}): {resp.text[:200]}", + ) + + @staticmethod + def _next_tag_page_url(resp: requests.Response) -> str | None: + link_header = resp.headers.get("Link", "") + if not link_header: + return None + match = re.search(r'<([^>]+)>;\s*rel="next"', link_header) + if match: + next_url = match.group(1) + if next_url.startswith("/"): + return f"{_REGISTRY_HOST}{next_url}" + return next_url + return None diff --git a/prowler/providers/image/lib/registry/factory.py b/prowler/providers/image/lib/registry/factory.py new file mode 100644 index 0000000000..5c0134fedd --- /dev/null +++ b/prowler/providers/image/lib/registry/factory.py @@ -0,0 +1,40 @@ +"""Factory for auto-detecting registry type and returning the appropriate adapter.""" + +from __future__ import annotations + +import re + +from prowler.providers.image.lib.registry.base import RegistryAdapter +from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter +from prowler.providers.image.lib.registry.oci_adapter import OciRegistryAdapter + +_DOCKER_HUB_PATTERN = re.compile( + r"^(https?://)?(docker\.io|registry-1\.docker\.io)(/|$)", re.IGNORECASE +) + + +def create_registry_adapter( + registry_url: str, + username: str | None = None, + password: str | None = None, + token: str | None = None, + verify_ssl: bool = True, +) -> RegistryAdapter: + """Auto-detect registry type from URL and return the appropriate adapter.""" + if _DOCKER_HUB_PATTERN.search(registry_url): + return DockerHubAdapter( + registry_url=registry_url, + username=username, + password=password, + token=token, + verify_ssl=verify_ssl, + ) + # ECR and other non-Docker-Hub registries implement the OCI Distribution Spec, + # so they are handled by the generic OCI adapter. + return OciRegistryAdapter( + registry_url=registry_url, + username=username, + password=password, + token=token, + verify_ssl=verify_ssl, + ) diff --git a/prowler/providers/image/lib/registry/oci_adapter.py b/prowler/providers/image/lib/registry/oci_adapter.py new file mode 100644 index 0000000000..b7eb4c780b --- /dev/null +++ b/prowler/providers/image/lib/registry/oci_adapter.py @@ -0,0 +1,228 @@ +"""Generic OCI Distribution Spec registry adapter.""" + +from __future__ import annotations + +import base64 +import ipaddress +import re +from typing import TYPE_CHECKING +from urllib.parse import urlparse + +from prowler.lib.logger import logger +from prowler.providers.image.exceptions.exceptions import ( + ImageRegistryAuthError, + ImageRegistryCatalogError, + ImageRegistryNetworkError, +) +from prowler.providers.image.lib.registry.base import RegistryAdapter + +if TYPE_CHECKING: + import requests + + +class OciRegistryAdapter(RegistryAdapter): + """Adapter for registries implementing OCI Distribution Spec.""" + + def __init__( + self, + registry_url: str, + username: str | None = None, + password: str | None = None, + token: str | None = None, + verify_ssl: bool = True, + ) -> None: + super().__init__(registry_url, username, password, token, verify_ssl) + self._base_url = self._normalise_url(registry_url) + self._bearer_token: str | None = None + self._basic_auth_verified = False + + @staticmethod + def _normalise_url(url: str) -> str: + url = url.rstrip("/") + if not url.startswith(("http://", "https://")): + url = f"https://{url}" + return url + + def list_repositories(self) -> list[str]: + self._ensure_auth() + repositories: list[str] = [] + url = f"{self._base_url}/v2/_catalog" + params: dict = {"n": 200} + while url: + resp = self._authed_request("GET", url, params=params) + if resp.status_code == 404: + raise ImageRegistryCatalogError( + file=__file__, + message=f"Registry at {self.registry_url} does not support catalog listing (/_catalog returned 404). Use --image or --image-list instead.", + ) + self._check_response(resp, "catalog listing") + data = resp.json() + repositories.extend(data.get("repositories", [])) + url = self._next_page_url(resp) + params = {} + return repositories + + def list_tags(self, repository: str) -> list[str]: + self._ensure_auth(repository=repository) + tags: list[str] = [] + url = f"{self._base_url}/v2/{repository}/tags/list" + params: dict = {"n": 200} + while url: + resp = self._authed_request("GET", url, params=params) + self._check_response(resp, f"tag listing for {repository}") + data = resp.json() + tags.extend(data.get("tags", []) or []) + url = self._next_page_url(resp) + params = {} + return tags + + def _ensure_auth(self, repository: str | None = None) -> None: + if self._bearer_token: + return + if self._basic_auth_verified: + return + if self.token: + self._bearer_token = self.token + return + ping_url = f"{self._base_url}/v2/" + resp = self._request_with_retry("GET", ping_url) + if resp.status_code == 200: + return + if resp.status_code == 401: + www_auth = resp.headers.get("Www-Authenticate", "") + + if not www_auth.lower().startswith("bearer"): + # Basic auth challenge (e.g., AWS ECR) + if self.username and self.password: + self._basic_auth_verified = True + return + raise ImageRegistryAuthError( + file=__file__, + message=( + f"Registry {self.registry_url} requires authentication " + f"but no credentials provided. " + f"Set REGISTRY_USERNAME and REGISTRY_PASSWORD." + ), + ) + + # Bearer token exchange (standard OCI flow) + self._bearer_token = self._obtain_bearer_token(www_auth, repository) + return + if resp.status_code == 403: + raise ImageRegistryAuthError( + file=__file__, + message=f"Access denied to registry {self.registry_url} (HTTP 403). Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + raise ImageRegistryNetworkError( + file=__file__, + message=f"Unexpected HTTP {resp.status_code} from registry {self.registry_url} during auth check.", + ) + + def _obtain_bearer_token( + self, www_authenticate: str, repository: str | None = None + ) -> str: + match = re.search(r'realm="([^"]+)"', www_authenticate) + if not match: + raise ImageRegistryAuthError( + file=__file__, + message=f"Cannot parse token endpoint from registry {self.registry_url}. Www-Authenticate: {www_authenticate[:200]}", + ) + realm = match.group(1) + self._validate_realm_url(realm) + params: dict = {} + service_match = re.search(r'service="([^"]+)"', www_authenticate) + if service_match: + params["service"] = service_match.group(1) + scope_match = re.search(r'scope="([^"]+)"', www_authenticate) + if scope_match: + params["scope"] = scope_match.group(1) + elif repository: + params["scope"] = f"repository:{repository}:pull" + auth = None + if self.username and self.password: + auth = (self.username, self.password) + resp = self._request_with_retry("GET", realm, params=params, auth=auth) + if resp.status_code != 200: + raise ImageRegistryAuthError( + file=__file__, + message=f"Failed to obtain bearer token from {realm} (HTTP {resp.status_code}). Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + data = resp.json() + token = data.get("token") or data.get("access_token", "") + if not token: + raise ImageRegistryAuthError( + file=__file__, + message=f"Token endpoint {realm} returned an empty token. Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + return token + + @staticmethod + def _validate_realm_url(realm: str) -> None: + parsed = urlparse(realm) + if parsed.scheme not in ("http", "https"): + raise ImageRegistryAuthError( + file=__file__, + message=f"Bearer token realm has disallowed scheme: {parsed.scheme}. Only http/https are allowed.", + ) + if parsed.scheme == "http": + logger.warning(f"Bearer token realm uses HTTP (not HTTPS): {realm}") + hostname = parsed.hostname or "" + try: + addr = ipaddress.ip_address(hostname) + if addr.is_private or addr.is_loopback or addr.is_link_local: + raise ImageRegistryAuthError( + file=__file__, + message=f"Bearer token realm points to a private/loopback address: {hostname}. This may indicate an SSRF attempt.", + ) + except ValueError: + pass + + def _resolve_basic_credentials(self) -> tuple[str | None, str | None]: + """Decode pre-encoded base64 auth tokens (e.g., from aws ecr get-authorization-token). + + Returns (username, password) — decoded if the password is a base64 token + containing 'username:real_password', otherwise returned as-is. + """ + if not self.password: + return self.username, self.password + try: + decoded = base64.b64decode(self.password).decode("utf-8") + if decoded.startswith(f"{self.username}:"): + return self.username, decoded[len(self.username) + 1 :] + except (ValueError, UnicodeDecodeError): + logger.debug("Password is not a base64-encoded auth token, using as-is") + return self.username, self.password + + def _authed_request(self, method: str, url: str, **kwargs) -> requests.Response: + resp = self._do_authed_request(method, url, **kwargs) + if resp.status_code == 401 and self._bearer_token: + logger.debug( + f"Bearer token rejected (HTTP 401), re-authenticating to {self.registry_url}" + ) + self._bearer_token = None + self._ensure_auth() + resp = self._do_authed_request(method, url, **kwargs) + return resp + + def _do_authed_request(self, method: str, url: str, **kwargs) -> requests.Response: + headers = kwargs.pop("headers", {}) + if self._bearer_token: + headers["Authorization"] = f"Bearer {self._bearer_token}" + elif self.username and self.password: + user, pwd = self._resolve_basic_credentials() + kwargs.setdefault("auth", (user, pwd)) + kwargs["headers"] = headers + return self._request_with_retry(method, url, **kwargs) + + def _check_response(self, resp: requests.Response, context: str) -> None: + if resp.status_code == 200: + return + if resp.status_code in (401, 403): + raise ImageRegistryAuthError( + file=__file__, + message=f"Authentication failed for {context} on {self.registry_url} (HTTP {resp.status_code}). Check REGISTRY_USERNAME and REGISTRY_PASSWORD.", + ) + raise ImageRegistryNetworkError( + file=__file__, + message=f"Unexpected error during {context} on {self.registry_url} (HTTP {resp.status_code}): {resp.text[:200]}", + ) diff --git a/tests/providers/image/image_fixtures.py b/tests/providers/image/image_fixtures.py index a51f934bda..920a7f225a 100644 --- a/tests/providers/image/image_fixtures.py +++ b/tests/providers/image/image_fixtures.py @@ -45,8 +45,16 @@ SAMPLE_UNKNOWN_SEVERITY_FINDING = { "Description": "An issue with unknown severity.", } +# Sample image SHA for testing (first 12 chars of a sha256 digest) +SAMPLE_IMAGE_SHA = "c1aabb73d233" +SAMPLE_IMAGE_ID = f"sha256:{SAMPLE_IMAGE_SHA}abcdef1234567890" + # Full Trivy JSON output structure with a single vulnerability SAMPLE_TRIVY_IMAGE_OUTPUT = { + "Metadata": { + "ImageID": SAMPLE_IMAGE_ID, + "RepoDigests": [f"alpine@sha256:{SAMPLE_IMAGE_SHA}abcdef1234567890"], + }, "Results": [ { "Target": "alpine:3.18 (alpine 3.18.0)", @@ -55,11 +63,15 @@ SAMPLE_TRIVY_IMAGE_OUTPUT = { "Secrets": [], "Misconfigurations": [], } - ] + ], } # Full Trivy JSON output with mixed finding types SAMPLE_TRIVY_MULTI_TYPE_OUTPUT = { + "Metadata": { + "ImageID": SAMPLE_IMAGE_ID, + "RepoDigests": [f"myimage@sha256:{SAMPLE_IMAGE_SHA}abcdef1234567890"], + }, "Results": [ { "Target": "myimage:latest (debian 12)", @@ -68,7 +80,36 @@ SAMPLE_TRIVY_MULTI_TYPE_OUTPUT = { "Secrets": [SAMPLE_SECRET_FINDING], "Misconfigurations": [SAMPLE_MISCONFIGURATION_FINDING], } - ] + ], +} + +# Trivy output with only RepoDigests (no ImageID) for fallback testing +SAMPLE_TRIVY_REPO_DIGEST_ONLY_OUTPUT = { + "Metadata": { + "RepoDigests": ["alpine@sha256:e5f6g7h8i9j0abcdef1234567890"], + }, + "Results": [ + { + "Target": "alpine:3.18 (alpine 3.18.0)", + "Type": "alpine", + "Vulnerabilities": [SAMPLE_VULNERABILITY_FINDING], + "Secrets": [], + "Misconfigurations": [], + } + ], +} + +# Trivy output with no Metadata at all +SAMPLE_TRIVY_NO_METADATA_OUTPUT = { + "Results": [ + { + "Target": "alpine:3.18 (alpine 3.18.0)", + "Type": "alpine", + "Vulnerabilities": [SAMPLE_VULNERABILITY_FINDING], + "Secrets": [], + "Misconfigurations": [], + } + ], } @@ -90,3 +131,13 @@ def get_invalid_trivy_output(): def get_multi_type_trivy_output(): """Return Trivy output with multiple finding types as string.""" return json.dumps(SAMPLE_TRIVY_MULTI_TYPE_OUTPUT) + + +def get_repo_digest_only_trivy_output(): + """Return Trivy output with only RepoDigests (no ImageID) as string.""" + return json.dumps(SAMPLE_TRIVY_REPO_DIGEST_ONLY_OUTPUT) + + +def get_no_metadata_trivy_output(): + """Return Trivy output with no Metadata as string.""" + return json.dumps(SAMPLE_TRIVY_NO_METADATA_OUTPUT) diff --git a/tests/providers/image/image_provider_test.py b/tests/providers/image/image_provider_test.py index 659d03d527..fc36b9e617 100644 --- a/tests/providers/image/image_provider_test.py +++ b/tests/providers/image/image_provider_test.py @@ -7,15 +7,20 @@ import pytest from prowler.lib.check.models import CheckReportImage from prowler.providers.image.exceptions.exceptions import ( - ImageDockerLoginError, - ImageDockerNotFoundError, + ImageInvalidConfigScannerError, + ImageInvalidNameError, + ImageInvalidScannerError, + ImageInvalidSeverityError, + ImageInvalidTimeoutError, ImageListFileNotFoundError, + ImageListFileReadError, ImageNoImagesProvidedError, ImageScanError, ImageTrivyBinaryNotFoundError, ) from prowler.providers.image.image_provider import ImageProvider from tests.providers.image.image_fixtures import ( + SAMPLE_IMAGE_SHA, SAMPLE_MISCONFIGURATION_FINDING, SAMPLE_SECRET_FINDING, SAMPLE_UNKNOWN_SEVERITY_FINDING, @@ -23,6 +28,8 @@ from tests.providers.image.image_fixtures import ( get_empty_trivy_output, get_invalid_trivy_output, get_multi_type_trivy_output, + get_no_metadata_trivy_output, + get_repo_digest_only_trivy_output, get_sample_trivy_json_output, ) @@ -46,6 +53,7 @@ class TestImageProvider: assert provider.type == "image" assert provider.images == ["alpine:3.18"] assert provider.scanners == ["vuln", "secret"] + assert provider.image_config_scanners == [] assert provider.trivy_severity == [] assert provider.ignore_unfixed is False assert provider.timeout == "5m" @@ -115,22 +123,27 @@ class TestImageProvider: provider = _make_provider() report = provider._process_finding( SAMPLE_VULNERABILITY_FINDING, + "alpine:3.18", "alpine:3.18 (alpine 3.18.0)", - "alpine", + image_sha="c1aabb73d233", ) assert isinstance(report, CheckReportImage) assert report.status == "FAIL" assert report.check_metadata.CheckID == "CVE-2024-1234" assert report.check_metadata.Severity == "high" - assert report.check_metadata.ServiceName == "alpine" + assert report.check_metadata.ServiceName == "container-image" assert report.check_metadata.ResourceType == "container-image" assert report.check_metadata.ResourceGroup == "container" assert report.package_name == "openssl" assert report.installed_version == "1.1.1k-r0" assert report.fixed_version == "1.1.1l-r0" - assert report.resource_name == "alpine:3.18 (alpine 3.18.0)" + assert report.resource_name == "alpine:3.18" + assert report.image_sha == "c1aabb73d233" + assert report.resource_details == "alpine:3.18 (alpine 3.18.0)" assert report.region == "container" + assert report.check_metadata.Categories == ["vulnerability"] + assert report.check_metadata.RelatedUrl == "" def test_process_finding_secret(self): """Test processing a secret finding (identified by RuleID).""" @@ -138,14 +151,15 @@ class TestImageProvider: report = provider._process_finding( SAMPLE_SECRET_FINDING, "myimage:latest", - "secret", + "myimage:latest (debian 12)", ) assert isinstance(report, CheckReportImage) assert report.status == "FAIL" assert report.check_metadata.CheckID == "aws-access-key-id" assert report.check_metadata.Severity == "critical" - assert report.check_metadata.ServiceName == "secret" + assert report.check_metadata.ServiceName == "container-image" + assert report.check_metadata.Categories == ["secrets"] def test_process_finding_misconfiguration(self): """Test processing a misconfiguration finding (identified by ID).""" @@ -153,13 +167,14 @@ class TestImageProvider: report = provider._process_finding( SAMPLE_MISCONFIGURATION_FINDING, "myimage:latest", - "misconfiguration", + "myimage:latest (debian 12)", ) assert isinstance(report, CheckReportImage) assert report.check_metadata.CheckID == "DS001" assert report.check_metadata.Severity == "medium" - assert report.check_metadata.ServiceName == "misconfiguration" + assert report.check_metadata.ServiceName == "container-image" + assert report.check_metadata.Categories == [] def test_process_finding_unknown_severity(self): """Test that UNKNOWN severity is mapped to informational.""" @@ -167,7 +182,7 @@ class TestImageProvider: report = provider._process_finding( SAMPLE_UNKNOWN_SEVERITY_FINDING, "myimage:latest", - "alpine", + "myimage:latest (alpine 3.18.0)", ) assert report.check_metadata.Severity == "informational" @@ -186,6 +201,9 @@ class TestImageProvider: assert len(reports) == 1 assert reports[0].check_metadata.CheckID == "CVE-2024-1234" + assert reports[0].image_sha == SAMPLE_IMAGE_SHA + assert reports[0].resource_name == "alpine:3.18" + assert reports[0].check_metadata.ServiceName == "container-image" @patch("subprocess.run") def test_run_scan_empty_output(self, mock_subprocess): @@ -385,6 +403,51 @@ class TestImageProvider: for _ in provider._scan_single_image("private/image:latest"): pass + @patch("subprocess.run") + def test_sha_extraction_from_image_id(self, mock_subprocess): + """Test that image_sha is extracted from Trivy Metadata.ImageID.""" + provider = _make_provider() + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_sample_trivy_json_output(), stderr="" + ) + + reports = [] + for batch in provider._scan_single_image("alpine:3.18"): + reports.extend(batch) + + assert len(reports) == 1 + assert reports[0].image_sha == SAMPLE_IMAGE_SHA + + @patch("subprocess.run") + def test_sha_extraction_fallback_to_repo_digests(self, mock_subprocess): + """Test that image_sha falls back to RepoDigests when ImageID is absent.""" + provider = _make_provider() + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_repo_digest_only_trivy_output(), stderr="" + ) + + reports = [] + for batch in provider._scan_single_image("alpine:3.18"): + reports.extend(batch) + + assert len(reports) == 1 + assert reports[0].image_sha == "e5f6g7h8i9j0" + + @patch("subprocess.run") + def test_sha_extraction_no_metadata(self, mock_subprocess): + """Test that image_sha is empty when no Metadata is present.""" + provider = _make_provider() + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_no_metadata_trivy_output(), stderr="" + ) + + reports = [] + for batch in provider._scan_single_image("alpine:3.18"): + reports.extend(batch) + + assert len(reports) == 1 + assert reports[0].image_sha == "" + @patch("subprocess.run") def test_run_scan_propagates_scan_error(self, mock_subprocess): """Test that run_scan() re-raises ImageScanError instead of swallowing it.""" @@ -438,7 +501,9 @@ class TestImageProviderRegistryAuth: assert provider.auth_method == "Docker login" - @patch.dict(os.environ, {"TRIVY_USERNAME": "envuser", "TRIVY_PASSWORD": "envpass"}) + @patch.dict( + os.environ, {"REGISTRY_USERNAME": "envuser", "REGISTRY_PASSWORD": "envpass"} + ) def test_basic_auth_from_env_vars(self): """Test that env vars are used as fallback for basic auth.""" provider = _make_provider() @@ -447,7 +512,7 @@ class TestImageProviderRegistryAuth: assert provider.registry_password == "envpass" assert provider.auth_method == "Docker login" - @patch.dict(os.environ, {"TRIVY_REGISTRY_TOKEN": "env-token"}) + @patch.dict(os.environ, {"REGISTRY_TOKEN": "env-token"}) def test_token_auth_from_env_var(self): """Test that env var is used as fallback for token auth.""" provider = _make_provider() @@ -455,7 +520,9 @@ class TestImageProviderRegistryAuth: assert provider.registry_token == "env-token" assert provider.auth_method == "Registry token" - @patch.dict(os.environ, {"TRIVY_USERNAME": "envuser", "TRIVY_PASSWORD": "envpass"}) + @patch.dict( + os.environ, {"REGISTRY_USERNAME": "envuser", "REGISTRY_PASSWORD": "envpass"} + ) def test_explicit_params_override_env_vars(self): """Test that explicit params take precedence over env vars.""" provider = _make_provider( @@ -475,16 +542,16 @@ class TestImageProviderRegistryAuth: assert "TRIVY_PASSWORD" not in env assert "TRIVY_REGISTRY_TOKEN" not in env - def test_build_trivy_env_basic_auth_no_env_vars(self): - """Test that _build_trivy_env does NOT inject TRIVY_USERNAME/PASSWORD (docker login handles it).""" + def test_build_trivy_env_basic_auth_sets_env_vars(self): + """Test that _build_trivy_env injects TRIVY_USERNAME/PASSWORD for native Trivy auth.""" provider = _make_provider( registry_username="myuser", registry_password="mypass", ) env = provider._build_trivy_env() - assert "TRIVY_USERNAME" not in env - assert "TRIVY_PASSWORD" not in env + assert env["TRIVY_USERNAME"] == "myuser" + assert env["TRIVY_PASSWORD"] == "mypass" def test_build_trivy_env_token_auth(self): """Test that _build_trivy_env injects registry token.""" @@ -494,8 +561,8 @@ class TestImageProviderRegistryAuth: assert env["TRIVY_REGISTRY_TOKEN"] == "my-token" @patch("subprocess.run") - def test_execute_trivy_no_trivy_env_with_basic_auth(self, mock_subprocess): - """Test that _execute_trivy does NOT set TRIVY_USERNAME/PASSWORD (docker login handles auth).""" + def test_execute_trivy_sets_trivy_env_with_basic_auth(self, mock_subprocess): + """Test that _execute_trivy sets TRIVY_USERNAME/PASSWORD for native Trivy auth.""" provider = _make_provider( registry_username="myuser", registry_password="mypass", @@ -508,12 +575,12 @@ class TestImageProviderRegistryAuth: call_kwargs = mock_subprocess.call_args env = call_kwargs.kwargs.get("env") or call_kwargs[1].get("env") - assert "TRIVY_USERNAME" not in env - assert "TRIVY_PASSWORD" not in env + assert env["TRIVY_USERNAME"] == "myuser" + assert env["TRIVY_PASSWORD"] == "mypass" @patch("subprocess.run") def test_test_connection_with_basic_auth(self, mock_subprocess): - """Test test_connection does docker login, pull, trivy, logout and does NOT set TRIVY_* env vars.""" + """Test test_connection uses Trivy native auth with TRIVY_USERNAME/PASSWORD env vars.""" mock_subprocess.return_value = MagicMock(returncode=0, stderr="") result = ImageProvider.test_connection( @@ -523,29 +590,12 @@ class TestImageProviderRegistryAuth: ) assert result.is_connected is True - # Should have 4 subprocess calls: docker login, docker pull, trivy, docker logout - assert mock_subprocess.call_count == 4 - login_call = mock_subprocess.call_args_list[0] - assert login_call.args[0] == [ - "docker", - "login", - "--username", - "myuser", - "--password-stdin", - "private.registry.io", - ] - assert login_call.kwargs["input"] == "mypass" - - pull_call = mock_subprocess.call_args_list[1] - assert pull_call.args[0] == ["docker", "pull", "private.registry.io/myapp:v1"] - - trivy_call = mock_subprocess.call_args_list[2] + assert mock_subprocess.call_count == 1 + trivy_call = mock_subprocess.call_args + assert trivy_call.args[0][0] == "trivy" env = trivy_call.kwargs.get("env") or trivy_call[1].get("env") - assert "TRIVY_USERNAME" not in env - assert "TRIVY_PASSWORD" not in env - - logout_call = mock_subprocess.call_args_list[3] - assert logout_call.args[0] == ["docker", "logout", "private.registry.io"] + assert env["TRIVY_USERNAME"] == "myuser" + assert env["TRIVY_PASSWORD"] == "mypass" @patch("subprocess.run") def test_test_connection_with_token(self, mock_subprocess): @@ -616,159 +666,7 @@ class TestExtractRegistry: assert ImageProvider._extract_registry("nginx") is None -class TestDockerLogin: - @patch("subprocess.run") - def test_login_success(self, mock_subprocess): - """Test successful docker login.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) - - provider._docker_login("ghcr.io") - - mock_subprocess.assert_called_once() - call_args = mock_subprocess.call_args - assert call_args.args[0] == [ - "docker", - "login", - "--username", - "myuser", - "--password-stdin", - "ghcr.io", - ] - assert call_args.kwargs["input"] == "mypass" - assert "ghcr.io" in provider._logged_in_registries - - @patch("subprocess.run") - def test_login_docker_hub(self, mock_subprocess): - """Test docker login for Docker Hub (registry=None).""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) - - provider._docker_login(None) - - call_args = mock_subprocess.call_args - assert call_args.args[0] == [ - "docker", - "login", - "--username", - "myuser", - "--password-stdin", - ] - assert None in provider._logged_in_registries - - @patch("subprocess.run") - def test_login_failure_raises(self, mock_subprocess): - """Test docker login failure raises ImageDockerLoginError.""" - mock_subprocess.return_value = MagicMock( - returncode=1, stderr="unauthorized: incorrect username or password" - ) - provider = _make_provider( - registry_username="myuser", - registry_password="badpass", - ) - - with pytest.raises(ImageDockerLoginError): - provider._docker_login("ghcr.io") - - assert "ghcr.io" not in provider._logged_in_registries - - @patch("subprocess.run") - def test_login_docker_not_found(self, mock_subprocess): - """Test docker binary not found raises ImageDockerNotFoundError.""" - mock_subprocess.side_effect = FileNotFoundError("docker not found") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) - - with pytest.raises(ImageDockerNotFoundError): - provider._docker_login("ghcr.io") - - @patch("subprocess.run") - def test_login_password_via_stdin_not_args(self, mock_subprocess): - """Test that password is passed via stdin, never in command args.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider( - registry_username="myuser", - registry_password="s3cret!", - ) - - provider._docker_login("ghcr.io") - - call_args = mock_subprocess.call_args - cmd = call_args.args[0] - assert "s3cret!" not in cmd - assert call_args.kwargs["input"] == "s3cret!" - - -class TestDockerLogout: - @patch("subprocess.run") - def test_logout_success(self, mock_subprocess): - """Test successful docker logout.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider() - - provider._docker_logout("ghcr.io") - - mock_subprocess.assert_called_once_with( - ["docker", "logout", "ghcr.io"], - capture_output=True, - text=True, - ) - - @patch("subprocess.run") - def test_logout_docker_hub(self, mock_subprocess): - """Test docker logout for Docker Hub (registry=None).""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider() - - provider._docker_logout(None) - - mock_subprocess.assert_called_once_with( - ["docker", "logout"], - capture_output=True, - text=True, - ) - - @patch("subprocess.run") - def test_logout_failure_warns(self, mock_subprocess): - """Test docker logout failure only warns, never raises.""" - mock_subprocess.return_value = MagicMock(returncode=1, stderr="some error") - provider = _make_provider() - - provider._docker_logout("ghcr.io") - - @patch("subprocess.run") - def test_logout_exception_warns(self, mock_subprocess): - """Test docker logout handles exceptions gracefully.""" - mock_subprocess.side_effect = OSError("docker crashed") - provider = _make_provider() - - provider._docker_logout("ghcr.io") - - class TestCleanup: - @patch("subprocess.run") - def test_cleanup_calls_logout_for_each_registry(self, mock_subprocess): - """Test cleanup calls docker logout for each logged-in registry.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) - provider._logged_in_registries = {"ghcr.io", None} - - provider.cleanup() - - assert mock_subprocess.call_count == 2 - assert len(provider._logged_in_registries) == 0 - def test_cleanup_idempotent(self): """Test cleanup is safe to call multiple times.""" provider = _make_provider() @@ -776,213 +674,257 @@ class TestCleanup: provider.cleanup() provider.cleanup() - assert len(provider._logged_in_registries) == 0 - @patch("subprocess.run") - def test_cleanup_clears_set(self, mock_subprocess): - """Test cleanup clears the _logged_in_registries set.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") +class TestImageProviderInputValidation: + def test_invalid_timeout_format_raises_error(self): + """Test that a non-matching timeout string raises ImageInvalidTimeoutError.""" + with pytest.raises(ImageInvalidTimeoutError): + _make_provider(timeout="invalid") + + def test_invalid_timeout_no_unit_raises_error(self): + """Test that a numeric timeout without a unit raises ImageInvalidTimeoutError.""" + with pytest.raises(ImageInvalidTimeoutError): + _make_provider(timeout="300") + + def test_invalid_timeout_wrong_unit_raises_error(self): + """Test that a timeout with an unsupported unit raises ImageInvalidTimeoutError.""" + with pytest.raises(ImageInvalidTimeoutError): + _make_provider(timeout="5d") + + def test_valid_timeout_seconds(self): + """Test that a seconds-based timeout is accepted.""" + provider = _make_provider(timeout="300s") + assert provider.timeout == "300s" + + def test_valid_timeout_hours(self): + """Test that an hours-based timeout is accepted.""" + provider = _make_provider(timeout="1h") + assert provider.timeout == "1h" + + def test_invalid_scanner_raises_error(self): + """Test that an invalid scanner name raises ImageInvalidScannerError.""" + with pytest.raises(ImageInvalidScannerError): + _make_provider(scanners=["vuln", "bad"]) + + def test_invalid_severity_raises_error(self): + """Test that an invalid severity level raises ImageInvalidSeverityError.""" + with pytest.raises(ImageInvalidSeverityError): + _make_provider(trivy_severity=["HIGH", "SUPER_HIGH"]) + + def test_valid_all_scanners(self): + """Test that all valid scanner choices are accepted.""" + provider = _make_provider(scanners=["vuln", "secret", "misconfig", "license"]) + assert provider.scanners == ["vuln", "secret", "misconfig", "license"] + + def test_valid_all_severities(self): + """Test that all valid severity choices are accepted.""" provider = _make_provider( - registry_username="myuser", - registry_password="mypass", + trivy_severity=["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"] ) - provider._logged_in_registries = {"ghcr.io"} + assert provider.trivy_severity == [ + "CRITICAL", + "HIGH", + "MEDIUM", + "LOW", + "UNKNOWN", + ] - provider.cleanup() + def test_image_config_scanners_defaults_to_empty(self): + """Test that image_config_scanners defaults to an empty list.""" + provider = _make_provider() + assert provider.image_config_scanners == [] - assert provider._logged_in_registries == set() + def test_valid_image_config_scanners(self): + """Test that valid image config scanners are accepted.""" + provider = _make_provider(image_config_scanners=["misconfig", "secret"]) + assert provider.image_config_scanners == ["misconfig", "secret"] - -class TestDockerPull: - @patch("subprocess.run") - def test_pull_success(self, mock_subprocess): - """Test successful docker pull.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) - - provider._docker_pull("ghcr.io/user/image:tag") - - mock_subprocess.assert_called_once_with( - ["docker", "pull", "ghcr.io/user/image:tag"], - capture_output=True, - text=True, - ) + def test_invalid_image_config_scanner_raises_error(self): + """Test that an invalid image config scanner raises ImageInvalidConfigScannerError.""" + with pytest.raises(ImageInvalidConfigScannerError): + _make_provider(image_config_scanners=["misconfig", "vuln"]) @patch("subprocess.run") - def test_pull_failure_raises_scan_error(self, mock_subprocess): - """Test docker pull failure raises ImageScanError.""" + def test_trivy_command_includes_image_config_scanners(self, mock_subprocess): + """Test that Trivy command includes --image-config-scanners when set.""" + provider = _make_provider(image_config_scanners=["misconfig", "secret"]) mock_subprocess.return_value = MagicMock( - returncode=1, stderr="Error: pull access denied" + returncode=0, stdout=get_empty_trivy_output(), stderr="" ) - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", + + for _ in provider._scan_single_image("alpine:3.18"): + pass + + call_args = mock_subprocess.call_args[0][0] + assert "--image-config-scanners" in call_args + idx = call_args.index("--image-config-scanners") + assert call_args[idx + 1] == "misconfig,secret" + + @patch("subprocess.run") + def test_trivy_command_omits_image_config_scanners_when_empty( + self, mock_subprocess + ): + """Test that Trivy command omits --image-config-scanners when empty.""" + provider = _make_provider(image_config_scanners=[]) + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_empty_trivy_output(), stderr="" ) + for _ in provider._scan_single_image("alpine:3.18"): + pass + + call_args = mock_subprocess.call_args[0][0] + assert "--image-config-scanners" not in call_args + + +class TestImageProviderErrorCategorization: + def test_categorize_auth_failure(self): + """Test that auth-related errors are categorized correctly.""" + result = ImageProvider._categorize_trivy_error( + "401 unauthorized: access denied" + ) + assert "Auth failure" in result + + def test_categorize_not_found(self): + """Test that not-found errors are categorized correctly.""" + result = ImageProvider._categorize_trivy_error( + "manifest unknown: image not found" + ) + assert "Image not found" in result + + def test_categorize_rate_limit(self): + """Test that rate-limit errors are categorized correctly.""" + result = ImageProvider._categorize_trivy_error("429 too many requests") + assert "Rate limited" in result + + def test_categorize_network_issue(self): + """Test that network errors are categorized correctly.""" + result = ImageProvider._categorize_trivy_error("connection refused to registry") + assert "Network issue" in result + + def test_categorize_unknown_error(self): + """Test that unrecognized errors are returned as-is.""" + msg = "some unknown trivy error" + result = ImageProvider._categorize_trivy_error(msg) + assert result == msg + + +class TestImageProviderNameValidation: + @pytest.mark.parametrize( + "bad_name", + [ + "alpine;rm -rf /", + "image|cat /etc/passwd", + "image&background", + "image$VAR", + "image`whoami`", + "image\ninjected", + "image\rinjected", + ], + ) + def test_image_provider_invalid_image_name_shell_chars(self, bad_name): + """Test that image names with shell metacharacters raise ImageInvalidNameError.""" + with pytest.raises(ImageInvalidNameError): + _make_provider(images=[bad_name]) + + def test_image_provider_invalid_image_name_empty(self): + """Test that an empty string image name raises ImageInvalidNameError.""" + with pytest.raises(ImageInvalidNameError): + _make_provider(images=[""]) + + @pytest.mark.parametrize( + "valid_name", + [ + "alpine:3.18", + "nginx:latest", + "registry.example.com/repo/image:tag", + "ghcr.io/owner/image:v1.2.3", + "myimage@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "localhost:5000/myimage:latest", + ], + ) + def test_image_provider_valid_image_names(self, valid_name): + """Test that various valid image name formats pass validation.""" + provider = _make_provider(images=[valid_name]) + assert valid_name in provider.images + + def test_image_provider_image_name_too_long(self): + """Test that a name exceeding 500 chars raises ImageInvalidNameError.""" + long_name = "a" * 501 + with pytest.raises(ImageInvalidNameError): + _make_provider(images=[long_name]) + + def test_image_provider_file_too_many_lines(self): + """Test that a file with more than MAX_IMAGE_LIST_LINES raises ImageListFileReadError.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f: + for i in range(10_001): + f.write(f"image{i}:latest\n") + f.flush() + file_path = f.name + + with pytest.raises(ImageListFileReadError): + _make_provider(images=None, image_list_file=file_path) + + +class TestScanPerImage: + @patch("subprocess.run") + def test_yields_per_image(self, mock_subprocess): + """Test that scan_per_image yields (name, findings) per image.""" + mock_subprocess.return_value = MagicMock( + returncode=0, stdout=get_sample_trivy_json_output(), stderr="" + ) + provider = _make_provider(images=["alpine:3.18", "nginx:latest"]) + + results = list(provider.scan_per_image()) + + assert len(results) == 2 + for name, findings in results: + assert isinstance(name, str) + assert isinstance(findings, list) + assert all(isinstance(f, CheckReportImage) for f in findings) + + @patch("subprocess.run") + def test_reraises_scan_error(self, mock_subprocess): + """Test that ImageScanError propagates from scan_per_image.""" + mock_subprocess.return_value = MagicMock( + returncode=1, stdout="", stderr="scan failed" + ) + provider = _make_provider(images=["alpine:3.18"]) + with pytest.raises(ImageScanError): - provider._docker_pull("private/image:tag") + list(provider.scan_per_image()) @patch("subprocess.run") - def test_pull_docker_not_found(self, mock_subprocess): - """Test docker binary not found raises ImageDockerNotFoundError.""" - mock_subprocess.side_effect = FileNotFoundError("docker not found") - provider = _make_provider( - registry_username="myuser", - registry_password="mypass", - ) + def test_skips_generic_error(self, mock_subprocess): + """Test that a generic RuntimeError in _scan_single_image yields empty findings and continues.""" - with pytest.raises(ImageDockerNotFoundError): - provider._docker_pull("alpine:3.18") - - -class TestDockerLoginIntegration: - @patch("subprocess.run") - def test_run_scan_calls_docker_login_with_credentials(self, mock_subprocess): - """Test that run_scan() calls docker login, docker pull, then trivy when credentials are set.""" - mock_subprocess.return_value = MagicMock( - returncode=0, stdout=get_sample_trivy_json_output(), stderr="" - ) - provider = _make_provider( - images=["ghcr.io/user/image:tag"], - registry_username="myuser", - registry_password="mypass", - ) - - reports = [] - for batch in provider.run_scan(): - reports.extend(batch) - - calls = mock_subprocess.call_args_list - assert calls[0].args[0][:2] == ["docker", "login"] - assert calls[1].args[0][:2] == ["docker", "pull"] - assert calls[2].args[0][:2] == ["trivy", "image"] - - @patch("subprocess.run") - def test_run_scan_no_docker_login_without_credentials(self, mock_subprocess): - """Test that run_scan() does NOT call docker login when no credentials.""" - mock_subprocess.return_value = MagicMock( - returncode=0, stdout=get_sample_trivy_json_output(), stderr="" - ) - provider = _make_provider() - - for batch in provider.run_scan(): - pass - - calls = mock_subprocess.call_args_list - assert all(call.args[0][0] == "trivy" for call in calls) - - @patch("subprocess.run") - def test_run_scan_no_docker_login_with_token_only(self, mock_subprocess): - """Test that run_scan() does NOT call docker login when only token is provided.""" - mock_subprocess.return_value = MagicMock( - returncode=0, stdout=get_sample_trivy_json_output(), stderr="" - ) - provider = _make_provider(registry_token="my-token") - - for batch in provider.run_scan(): - pass - - calls = mock_subprocess.call_args_list - assert all(call.args[0][0] == "trivy" for call in calls) - - @patch("subprocess.run") - def test_run_calls_cleanup_on_success(self, mock_subprocess): - """Test that run() calls cleanup after successful scan.""" - mock_subprocess.return_value = MagicMock( - returncode=0, stdout=get_sample_trivy_json_output(), stderr="" - ) - provider = _make_provider( - images=["ghcr.io/user/image:tag"], - registry_username="myuser", - registry_password="mypass", - ) - - provider.run() - - calls = mock_subprocess.call_args_list - # login, pull, trivy, logout - assert calls[0].args[0][:2] == ["docker", "login"] - assert calls[1].args[0][:2] == ["docker", "pull"] - assert calls[2].args[0][:2] == ["trivy", "image"] - assert calls[-1].args[0][:2] == ["docker", "logout"] - - @patch("subprocess.run") - def test_run_calls_cleanup_on_error(self, mock_subprocess): - """Test that run() calls cleanup even when scan errors.""" - - def side_effect(*args, **kwargs): - cmd = args[0] - if cmd[0] == "docker" and cmd[1] == "login": - return MagicMock(returncode=0, stderr="") - if cmd[0] == "docker" and cmd[1] == "pull": - return MagicMock(returncode=0, stderr="") - if cmd[0] == "trivy": - return MagicMock(returncode=1, stdout="", stderr="scan failed") - return MagicMock(returncode=0, stderr="") + def side_effect(cmd, **kwargs): + if "bad:image" in cmd: + raise RuntimeError("unexpected error") + return MagicMock( + returncode=0, stdout=get_sample_trivy_json_output(), stderr="" + ) mock_subprocess.side_effect = side_effect - provider = _make_provider( - images=["ghcr.io/user/image:tag"], - registry_username="myuser", - registry_password="mypass", - ) + provider = _make_provider(images=["bad:image", "alpine:3.18"]) - with pytest.raises(ImageScanError): - provider.run() + results = list(provider.scan_per_image()) - last_call = mock_subprocess.call_args_list[-1] - assert last_call.args[0][:2] == ["docker", "logout"] + assert len(results) == 2 + assert results[0][0] == "bad:image" + assert results[0][1] == [] + assert results[1][0] == "alpine:3.18" + assert len(results[1][1]) > 0 @patch("subprocess.run") - def test_run_scan_deduplicates_registries(self, mock_subprocess): - """Test that run_scan() deduplicates registries for docker login but pulls each image.""" + def test_calls_cleanup(self, mock_subprocess): + """Test that cleanup is called even after scan_per_image completes.""" mock_subprocess.return_value = MagicMock( returncode=0, stdout=get_sample_trivy_json_output(), stderr="" ) - provider = _make_provider( - images=["ghcr.io/user/image1:tag", "ghcr.io/user/image2:tag"], - registry_username="myuser", - registry_password="mypass", - ) + provider = _make_provider(images=["alpine:3.18"]) - for batch in provider.run_scan(): - pass + with mock.patch.object(provider, "cleanup") as mock_cleanup: + list(provider.scan_per_image()) - calls = mock_subprocess.call_args_list - docker_login_calls = [c for c in calls if c.args[0][:2] == ["docker", "login"]] - docker_pull_calls = [c for c in calls if c.args[0][:2] == ["docker", "pull"]] - trivy_calls = [c for c in calls if c.args[0][:2] == ["trivy", "image"]] - assert len(docker_login_calls) == 1 - assert len(docker_pull_calls) == 2 - assert len(trivy_calls) == 2 - - @patch("subprocess.run") - def test_test_connection_docker_login_docker_hub(self, mock_subprocess): - """Test test_connection does docker login and pull for Docker Hub images.""" - mock_subprocess.return_value = MagicMock(returncode=0, stderr="") - - result = ImageProvider.test_connection( - image="andoniaf/test-private:tag", - registry_username="myuser", - registry_password="mypass", - ) - - assert result.is_connected is True - assert mock_subprocess.call_count == 4 - login_call = mock_subprocess.call_args_list[0] - assert login_call.args[0] == [ - "docker", - "login", - "--username", - "myuser", - "--password-stdin", - ] - pull_call = mock_subprocess.call_args_list[1] - assert pull_call.args[0] == [ - "docker", - "pull", - "andoniaf/test-private:tag", - ] + mock_cleanup.assert_called_once() diff --git a/tests/providers/image/lib/__init__.py b/tests/providers/image/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/image/lib/registry/__init__.py b/tests/providers/image/lib/registry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/providers/image/lib/registry/test_arguments.py b/tests/providers/image/lib/registry/test_arguments.py new file mode 100644 index 0000000000..5b52d385fd --- /dev/null +++ b/tests/providers/image/lib/registry/test_arguments.py @@ -0,0 +1,223 @@ +from argparse import Namespace + +from prowler.providers.image.lib.arguments.arguments import validate_arguments + + +class TestValidateArguments: + def test_no_source_fails(self): + args = Namespace( + images=[], + image_list_file=None, + registry=None, + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--image" in msg + + def test_image_only_passes(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry=None, + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_image_list_only_passes(self): + args = Namespace( + images=[], + image_list_file="images.txt", + registry=None, + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_registry_only_passes(self): + args = Namespace( + images=[], + image_list_file=None, + registry="myregistry.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_image_filter_without_registry_fails(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry=None, + image_filter="^prod", + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--image-filter requires --registry" in msg + + def test_tag_filter_without_registry_fails(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry=None, + image_filter=None, + tag_filter="^v", + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--tag-filter requires --registry" in msg + + def test_max_images_without_registry_fails(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry=None, + image_filter=None, + tag_filter=None, + max_images=50, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--max-images requires --registry" in msg + + def test_registry_insecure_without_registry_fails(self): + args = Namespace( + images=[], + image_list_file="i.txt", + registry=None, + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=True, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--registry-insecure requires --registry" in msg + + def test_docker_hub_no_namespace_fails(self): + args = Namespace( + images=[], + image_list_file=None, + registry="docker.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "namespace" in msg.lower() + + def test_docker_hub_with_namespace_passes(self): + args = Namespace( + images=[], + image_list_file=None, + registry="docker.io/myorg", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_docker_hub_https_no_namespace_fails(self): + args = Namespace( + images=[], + image_list_file=None, + registry="https://docker.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "namespace" in msg.lower() + + def test_registry_with_filters_passes(self): + args = Namespace( + images=[], + image_list_file=None, + registry="myregistry.io", + image_filter="^prod", + tag_filter="^v", + max_images=100, + registry_insecure=True, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_registry_list_without_registry_fails(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry=None, + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=True, + ) + ok, msg = validate_arguments(args) + assert not ok + assert "--registry-list requires --registry" in msg + + def test_registry_list_with_registry_passes(self): + args = Namespace( + images=[], + image_list_file=None, + registry="myregistry.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=True, + ) + ok, _ = validate_arguments(args) + assert ok + + def test_combined_registry_and_image_passes(self): + args = Namespace( + images=["nginx:latest"], + image_list_file=None, + registry="myregistry.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + ) + ok, _ = validate_arguments(args) + assert ok diff --git a/tests/providers/image/lib/registry/test_dockerhub_adapter.py b/tests/providers/image/lib/registry/test_dockerhub_adapter.py new file mode 100644 index 0000000000..930e872bba --- /dev/null +++ b/tests/providers/image/lib/registry/test_dockerhub_adapter.py @@ -0,0 +1,243 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from prowler.providers.image.exceptions.exceptions import ( + ImageRegistryAuthError, + ImageRegistryCatalogError, + ImageRegistryNetworkError, +) +from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter + + +class TestDockerHubAdapterInit: + def test_extract_namespace_simple(self): + assert DockerHubAdapter._extract_namespace("docker.io/myorg") == "myorg" + + def test_extract_namespace_https(self): + assert DockerHubAdapter._extract_namespace("https://docker.io/myorg") == "myorg" + + def test_extract_namespace_registry1(self): + assert ( + DockerHubAdapter._extract_namespace("registry-1.docker.io/myorg") == "myorg" + ) + + def test_extract_namespace_empty(self): + assert DockerHubAdapter._extract_namespace("docker.io") == "" + + def test_extract_namespace_with_slash(self): + assert DockerHubAdapter._extract_namespace("docker.io/myorg/") == "myorg" + + +class TestDockerHubListRepositories: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_repos(self, mock_request): + # Hub login (now goes through requests.request via _request_with_retry) + login_resp = MagicMock(status_code=200) + login_resp.json.return_value = {"token": "jwt"} + # Repo listing + repos_resp = MagicMock(status_code=200) + repos_resp.json.return_value = { + "results": [{"name": "app1"}, {"name": "app2"}], + "next": None, + } + mock_request.side_effect = [login_resp, repos_resp] + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + repos = adapter.list_repositories() + assert repos == ["myorg/app1", "myorg/app2"] + + def test_list_repos_no_namespace_raises(self): + adapter = DockerHubAdapter("docker.io") + with pytest.raises(ImageRegistryCatalogError, match="namespace"): + adapter.list_repositories() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_repos_public_no_credentials(self, mock_request): + """When no credentials are provided, use the public /v2/repositories/{ns}/ endpoint.""" + repos_resp = MagicMock(status_code=200) + repos_resp.json.return_value = { + "results": [{"name": "repo1"}, {"name": "repo2"}], + "next": None, + } + mock_request.return_value = repos_resp + adapter = DockerHubAdapter("docker.io/publicns") + repos = adapter.list_repositories() + assert repos == ["publicns/repo1", "publicns/repo2"] + called_url = mock_request.call_args[0][1] + assert "/v2/repositories/publicns/" in called_url + assert "/v2/namespaces/" not in called_url + + +class TestDockerHubListTags: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_tags(self, mock_request): + # Token exchange (now goes through requests.request via _request_with_retry) + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {"token": "registry-token"} + # Tag listing + tags_resp = MagicMock(status_code=200, headers={}) + tags_resp.json.return_value = {"tags": ["latest", "v1.0"]} + mock_request.side_effect = [token_resp, tags_resp] + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + tags = adapter.list_tags("myorg/myapp") + assert tags == ["latest", "v1.0"] + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_tags_auth_failure(self, mock_request): + # Token exchange + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {"token": "tok"} + # Tag listing returns 401 + tags_resp = MagicMock(status_code=401) + mock_request.side_effect = [token_resp, tags_resp] + adapter = DockerHubAdapter("docker.io/myorg") + with pytest.raises(ImageRegistryAuthError): + adapter.list_tags("myorg/myapp") + + +class TestDockerHubLogin: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_login_failure(self, mock_request): + resp = MagicMock(status_code=401, text="invalid credentials") + mock_request.return_value = resp + adapter = DockerHubAdapter("docker.io/myorg", username="bad", password="creds") + with pytest.raises(ImageRegistryAuthError, match="login failed"): + adapter._hub_login() + + def test_login_skipped_without_credentials(self): + adapter = DockerHubAdapter("docker.io/myorg") + adapter._hub_login() # Should not raise + assert adapter._hub_jwt is None + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_login_401_includes_response_body(self, mock_request): + resp = MagicMock( + status_code=401, text='{"detail":"Incorrect authentication credentials"}' + ) + mock_request.return_value = resp + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + with pytest.raises( + ImageRegistryAuthError, match="Incorrect authentication credentials" + ): + adapter._hub_login() + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_login_500_retried_then_raises_network_error( + self, mock_request, mock_sleep + ): + mock_request.return_value = MagicMock(status_code=500) + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + with pytest.raises(ImageRegistryNetworkError, match="Server error"): + adapter._hub_login() + assert mock_request.call_count == 3 + + +class TestDockerHubRetry: + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_retry_on_429(self, mock_request, mock_sleep): + resp_429 = MagicMock(status_code=429) + resp_200 = MagicMock(status_code=200) + mock_request.side_effect = [resp_429, resp_200] + adapter = DockerHubAdapter("docker.io/myorg") + result = adapter._request_with_retry( + "GET", "https://hub.docker.com/v2/namespaces/myorg/repositories" + ) + assert result.status_code == 200 + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_connection_error_retries(self, mock_request, mock_sleep): + mock_request.side_effect = requests.exceptions.ConnectionError("fail") + adapter = DockerHubAdapter("docker.io/myorg") + with pytest.raises(ImageRegistryNetworkError): + adapter._request_with_retry("GET", "https://hub.docker.com") + assert mock_request.call_count == 3 + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_retry_on_500(self, mock_request, mock_sleep): + resp_500 = MagicMock(status_code=500) + resp_200 = MagicMock(status_code=200) + mock_request.side_effect = [resp_500, resp_200] + adapter = DockerHubAdapter("docker.io/myorg") + result = adapter._request_with_retry("GET", "https://hub.docker.com") + assert result.status_code == 200 + assert mock_request.call_count == 2 + mock_sleep.assert_called_once() + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_retry_exhausted_on_500_raises_network_error( + self, mock_request, mock_sleep + ): + mock_request.return_value = MagicMock(status_code=500) + adapter = DockerHubAdapter("docker.io/myorg") + with pytest.raises( + ImageRegistryNetworkError, match="Server error.*HTTP 500.*3 attempts" + ): + adapter._request_with_retry("GET", "https://hub.docker.com") + assert mock_request.call_count == 3 + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_4xx_not_retried(self, mock_request, mock_sleep): + mock_request.return_value = MagicMock(status_code=403) + adapter = DockerHubAdapter("docker.io/myorg") + result = adapter._request_with_retry("GET", "https://hub.docker.com") + assert result.status_code == 403 + assert mock_request.call_count == 1 + mock_sleep.assert_not_called() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_request_sends_user_agent(self, mock_request): + mock_request.return_value = MagicMock(status_code=200) + adapter = DockerHubAdapter("docker.io/myorg") + adapter._request_with_retry("GET", "https://hub.docker.com") + _, kwargs = mock_request.call_args + from prowler.config.config import prowler_version + + assert ( + kwargs["headers"]["User-Agent"] + == f"Prowler/{prowler_version} (registry-adapter)" + ) + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_retry_500_includes_response_body(self, mock_request, mock_sleep): + resp_500 = MagicMock(status_code=500, text="Cloudflare error") + mock_request.return_value = resp_500 + adapter = DockerHubAdapter("docker.io/myorg") + with pytest.raises(ImageRegistryNetworkError, match="Cloudflare error"): + adapter._request_with_retry("GET", "https://hub.docker.com") + + +class TestDockerHubEmptyTokens: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_empty_hub_jwt_raises(self, mock_request): + resp = MagicMock(status_code=200) + resp.json.return_value = {"token": ""} + mock_request.return_value = resp + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + with pytest.raises(ImageRegistryAuthError, match="empty JWT"): + adapter._hub_login() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_none_hub_jwt_raises(self, mock_request): + resp = MagicMock(status_code=200) + resp.json.return_value = {} + mock_request.return_value = resp + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + with pytest.raises(ImageRegistryAuthError, match="empty JWT"): + adapter._hub_login() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_empty_registry_token_raises(self, mock_request): + resp = MagicMock(status_code=200) + resp.json.return_value = {"token": ""} + mock_request.return_value = resp + adapter = DockerHubAdapter("docker.io/myorg", username="u", password="p") + with pytest.raises(ImageRegistryAuthError, match="empty token"): + adapter._get_registry_token("myorg/myapp") diff --git a/tests/providers/image/lib/registry/test_factory.py b/tests/providers/image/lib/registry/test_factory.py new file mode 100644 index 0000000000..74aab82d02 --- /dev/null +++ b/tests/providers/image/lib/registry/test_factory.py @@ -0,0 +1,34 @@ +from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter +from prowler.providers.image.lib.registry.factory import create_registry_adapter +from prowler.providers.image.lib.registry.oci_adapter import OciRegistryAdapter + + +class TestCreateRegistryAdapter: + def test_docker_hub_returns_dockerhub_adapter(self): + adapter = create_registry_adapter("docker.io/myorg") + assert isinstance(adapter, DockerHubAdapter) + + def test_oci_returns_oci_adapter(self): + adapter = create_registry_adapter("myregistry.io") + assert isinstance(adapter, OciRegistryAdapter) + + def test_ecr_returns_oci_adapter(self): + adapter = create_registry_adapter("123456789.dkr.ecr.us-east-1.amazonaws.com") + assert isinstance(adapter, OciRegistryAdapter) + + def test_passes_credentials(self): + adapter = create_registry_adapter( + "myregistry.io", + username="user", + password="pass", + token="tok", + verify_ssl=False, + ) + assert adapter.username == "user" + assert adapter.password == "pass" + assert adapter.token == "tok" + assert adapter.verify_ssl is False + + def test_registry_1_docker_io(self): + adapter = create_registry_adapter("registry-1.docker.io/myorg") + assert isinstance(adapter, DockerHubAdapter) diff --git a/tests/providers/image/lib/registry/test_oci_adapter.py b/tests/providers/image/lib/registry/test_oci_adapter.py new file mode 100644 index 0000000000..32b0202e2d --- /dev/null +++ b/tests/providers/image/lib/registry/test_oci_adapter.py @@ -0,0 +1,418 @@ +import base64 +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from prowler.providers.image.exceptions.exceptions import ( + ImageRegistryAuthError, + ImageRegistryCatalogError, + ImageRegistryNetworkError, +) +from prowler.providers.image.lib.registry.oci_adapter import OciRegistryAdapter + + +class TestOciAdapterInit: + def test_normalise_url_adds_https(self): + adapter = OciRegistryAdapter("myregistry.io") + assert adapter._base_url == "https://myregistry.io" + + def test_normalise_url_keeps_http(self): + adapter = OciRegistryAdapter("http://myregistry.io") + assert adapter._base_url == "http://myregistry.io" + + def test_normalise_url_strips_trailing_slash(self): + adapter = OciRegistryAdapter("https://myregistry.io/") + assert adapter._base_url == "https://myregistry.io" + + def test_stores_credentials(self): + adapter = OciRegistryAdapter( + "reg.io", username="u", password="p", token="t", verify_ssl=False + ) + assert adapter.username == "u" + assert adapter.password == "p" + assert adapter.token == "t" + assert adapter.verify_ssl is False + + +class TestOciAdapterAuth: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_with_token(self, mock_request): + adapter = OciRegistryAdapter("reg.io", token="my-token") + adapter._ensure_auth() + assert adapter._bearer_token == "my-token" + mock_request.assert_not_called() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_anonymous_ok(self, mock_request): + resp = MagicMock(status_code=200) + mock_request.return_value = resp + adapter = OciRegistryAdapter("reg.io") + adapter._ensure_auth() + assert adapter._bearer_token is None + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_bearer_challenge(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={ + "Www-Authenticate": 'Bearer realm="https://auth.example.com/token",service="registry"' + }, + ) + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {"token": "bearer-tok"} + mock_request.side_effect = [ping_resp, token_resp] + adapter = OciRegistryAdapter("reg.io", username="u", password="p") + adapter._ensure_auth() + assert adapter._bearer_token == "bearer-tok" + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_403_raises(self, mock_request): + resp = MagicMock(status_code=403) + mock_request.return_value = resp + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError): + adapter._ensure_auth() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_basic_challenge_with_creds(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={"Www-Authenticate": 'Basic realm="https://ecr.aws"'}, + ) + mock_request.return_value = ping_resp + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password="tok") + adapter._ensure_auth() + assert adapter._basic_auth_verified is True + assert adapter._bearer_token is None + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_ensure_auth_basic_challenge_no_creds(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={"Www-Authenticate": 'Basic realm="https://ecr.aws"'}, + ) + mock_request.return_value = ping_resp + adapter = OciRegistryAdapter("ecr.aws") + with pytest.raises(ImageRegistryAuthError): + adapter._ensure_auth() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_basic_auth_used_in_requests(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={"Www-Authenticate": 'Basic realm="https://ecr.aws"'}, + ) + catalog_resp = MagicMock(status_code=200, headers={}) + catalog_resp.json.return_value = {"repositories": ["myapp"]} + mock_request.side_effect = [ping_resp, catalog_resp] + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password="tok") + adapter._ensure_auth() + adapter._authed_request("GET", "https://ecr.aws/v2/_catalog") + # The catalog request should use Basic auth (auth kwarg), not Bearer header + call_kwargs = mock_request.call_args_list[1][1] + assert call_kwargs.get("auth") == ("AWS", "tok") + assert "Authorization" not in call_kwargs.get("headers", {}) + + def test_resolve_basic_credentials_decodes_base64_token(self): + raw_password = "real-jwt-password" + encoded = base64.b64encode(f"AWS:{raw_password}".encode()).decode() + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password=encoded) + user, pwd = adapter._resolve_basic_credentials() + assert user == "AWS" + assert pwd == raw_password + + def test_resolve_basic_credentials_passthrough_raw_password(self): + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password="plain-pass") + user, pwd = adapter._resolve_basic_credentials() + assert user == "AWS" + assert pwd == "plain-pass" + + def test_resolve_basic_credentials_passthrough_invalid_base64(self): + adapter = OciRegistryAdapter( + "ecr.aws", username="AWS", password="not!valid~base64" + ) + user, pwd = adapter._resolve_basic_credentials() + assert user == "AWS" + assert pwd == "not!valid~base64" + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_basic_auth_decodes_ecr_token_in_request(self, mock_request): + raw_password = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.abc" + encoded = base64.b64encode(f"AWS:{raw_password}".encode()).decode() + ping_resp = MagicMock( + status_code=401, + headers={"Www-Authenticate": 'Basic realm="https://ecr.aws"'}, + ) + catalog_resp = MagicMock(status_code=200, headers={}) + catalog_resp.json.return_value = {"repositories": ["myapp"]} + mock_request.side_effect = [ping_resp, catalog_resp] + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password=encoded) + adapter._ensure_auth() + adapter._authed_request("GET", "https://ecr.aws/v2/_catalog") + call_kwargs = mock_request.call_args_list[1][1] + assert call_kwargs.get("auth") == ("AWS", raw_password) + + def test_resolve_basic_credentials_none_password(self): + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password=None) + user, pwd = adapter._resolve_basic_credentials() + assert user == "AWS" + assert pwd is None + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_authed_request_retries_on_401_with_bearer(self, mock_request): + adapter = OciRegistryAdapter("reg.io", username="u", password="p") + adapter._bearer_token = "expired-token" + # First request: 401 (expired token) + resp_401 = MagicMock(status_code=401) + # _ensure_auth ping: 401 with bearer challenge + ping_resp = MagicMock( + status_code=401, + headers={ + "Www-Authenticate": 'Bearer realm="https://auth.reg.io/token",service="registry"' + }, + ) + # Token exchange: success + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {"token": "new-token"} + # Second request: 200 (new token works) + resp_200 = MagicMock(status_code=200) + mock_request.side_effect = [resp_401, ping_resp, token_resp, resp_200] + result = adapter._authed_request("GET", "https://reg.io/v2/myapp/tags/list") + assert result.status_code == 200 + assert adapter._bearer_token == "new-token" + assert mock_request.call_count == 4 + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_authed_request_no_retry_on_401_without_bearer(self, mock_request): + adapter = OciRegistryAdapter("reg.io", username="u", password="p") + adapter._basic_auth_verified = True + # No bearer token — using basic auth + resp_401 = MagicMock(status_code=401) + mock_request.return_value = resp_401 + result = adapter._authed_request("GET", "https://reg.io/v2/_catalog") + assert result.status_code == 401 + # Should only be called once (no retry for basic auth) + assert mock_request.call_count == 1 + + +class TestOciAdapterListRepositories: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_repos_single_page(self, mock_request): + ping_resp = MagicMock(status_code=200) + catalog_resp = MagicMock(status_code=200, headers={}) + catalog_resp.json.return_value = { + "repositories": ["app/frontend", "app/backend"] + } + mock_request.side_effect = [ping_resp, catalog_resp] + adapter = OciRegistryAdapter("reg.io") + repos = adapter.list_repositories() + assert repos == ["app/frontend", "app/backend"] + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_repos_paginated(self, mock_request): + ping_resp = MagicMock(status_code=200) + page1_resp = MagicMock( + status_code=200, + headers={"Link": '; rel="next"'}, + ) + page1_resp.json.return_value = {"repositories": ["a"]} + page2_resp = MagicMock(status_code=200, headers={}) + page2_resp.json.return_value = {"repositories": ["b"]} + mock_request.side_effect = [ping_resp, page1_resp, page2_resp] + adapter = OciRegistryAdapter("reg.io") + repos = adapter.list_repositories() + assert repos == ["a", "b"] + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_repos_404_raises(self, mock_request): + ping_resp = MagicMock(status_code=200) + catalog_resp = MagicMock(status_code=404) + mock_request.side_effect = [ping_resp, catalog_resp] + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryCatalogError): + adapter.list_repositories() + + +class TestOciAdapterListTags: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_tags(self, mock_request): + ping_resp = MagicMock(status_code=200) + tags_resp = MagicMock(status_code=200, headers={}) + tags_resp.json.return_value = {"tags": ["latest", "v1.0"]} + mock_request.side_effect = [ping_resp, tags_resp] + adapter = OciRegistryAdapter("reg.io") + tags = adapter.list_tags("myapp") + assert tags == ["latest", "v1.0"] + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_list_tags_null_tags(self, mock_request): + ping_resp = MagicMock(status_code=200) + tags_resp = MagicMock(status_code=200, headers={}) + tags_resp.json.return_value = {"tags": None} + mock_request.side_effect = [ping_resp, tags_resp] + adapter = OciRegistryAdapter("reg.io") + tags = adapter.list_tags("myapp") + assert tags == [] + + +class TestOciAdapterRetry: + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_retry_on_429(self, mock_request, mock_sleep): + resp_429 = MagicMock(status_code=429) + resp_200 = MagicMock(status_code=200) + mock_request.side_effect = [resp_429, resp_200] + adapter = OciRegistryAdapter("reg.io") + result = adapter._request_with_retry("GET", "https://reg.io/v2/") + assert result.status_code == 200 + mock_sleep.assert_called_once() + + @patch("prowler.providers.image.lib.registry.base.time.sleep") + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_connection_error_retries(self, mock_request, mock_sleep): + mock_request.side_effect = requests.exceptions.ConnectionError("failed") + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryNetworkError): + adapter._request_with_retry("GET", "https://reg.io/v2/") + assert mock_request.call_count == 3 + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_timeout_raises_immediately(self, mock_request): + mock_request.side_effect = requests.exceptions.Timeout("timeout") + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryNetworkError): + adapter._request_with_retry("GET", "https://reg.io/v2/") + assert mock_request.call_count == 1 + + +class TestOciAdapterNextPageUrl: + def test_no_link_header(self): + resp = MagicMock(headers={}) + assert OciRegistryAdapter._next_page_url(resp) is None + + def test_link_header_with_next(self): + resp = MagicMock( + headers={"Link": '; rel="next"'} + ) + assert ( + OciRegistryAdapter._next_page_url(resp) + == "https://reg.io/v2/_catalog?n=200&last=b" + ) + + def test_link_header_no_next(self): + resp = MagicMock( + headers={"Link": '; rel="prev"'} + ) + assert OciRegistryAdapter._next_page_url(resp) is None + + +class TestOciAdapterSSRF: + def test_reject_file_scheme(self): + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError, match="disallowed scheme"): + adapter._validate_realm_url("file:///etc/passwd") + + def test_reject_ftp_scheme(self): + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError, match="disallowed scheme"): + adapter._validate_realm_url("ftp://evil.com/token") + + def test_reject_private_ip(self): + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError, match="private/loopback"): + adapter._validate_realm_url("https://10.0.0.1/token") + + def test_reject_loopback(self): + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError, match="private/loopback"): + adapter._validate_realm_url("https://127.0.0.1/token") + + def test_reject_link_local(self): + adapter = OciRegistryAdapter("reg.io") + with pytest.raises(ImageRegistryAuthError, match="private/loopback"): + adapter._validate_realm_url("https://169.254.169.254/latest/meta-data") + + def test_accept_public_https(self): + adapter = OciRegistryAdapter("reg.io") + # Should not raise + adapter._validate_realm_url("https://auth.example.com/token") + + def test_accept_hostname_not_ip(self): + adapter = OciRegistryAdapter("reg.io") + # Hostnames (not IPs) should pass even if they resolve to private IPs + adapter._validate_realm_url("https://internal.corp.com/token") + + +class TestOciAdapterEmptyToken: + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_empty_bearer_token_raises(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={ + "Www-Authenticate": 'Bearer realm="https://auth.example.com/token",service="registry"' + }, + ) + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {"token": "", "access_token": ""} + mock_request.side_effect = [ping_resp, token_resp] + adapter = OciRegistryAdapter("reg.io", username="u", password="p") + with pytest.raises(ImageRegistryAuthError, match="empty token"): + adapter._ensure_auth() + + @patch("prowler.providers.image.lib.registry.base.requests.request") + def test_none_bearer_token_raises(self, mock_request): + ping_resp = MagicMock( + status_code=401, + headers={ + "Www-Authenticate": 'Bearer realm="https://auth.example.com/token",service="registry"' + }, + ) + token_resp = MagicMock(status_code=200) + token_resp.json.return_value = {} + mock_request.side_effect = [ping_resp, token_resp] + adapter = OciRegistryAdapter("reg.io", username="u", password="p") + with pytest.raises(ImageRegistryAuthError, match="empty token"): + adapter._ensure_auth() + + +class TestOciAdapterNarrowExcept: + def test_invalid_utf8_base64_falls_through(self): + # Create a base64 string that decodes to invalid UTF-8 + invalid_bytes = base64.b64encode(b"\xff\xfe").decode() + adapter = OciRegistryAdapter("ecr.aws", username="AWS", password=invalid_bytes) + user, pwd = adapter._resolve_basic_credentials() + assert user == "AWS" + assert pwd == invalid_bytes + + +class TestCredentialRedaction: + def test_getstate_redacts_credentials(self): + adapter = OciRegistryAdapter( + "reg.io", username="u", password="secret", token="tok" + ) + state = adapter.__getstate__() + assert state["_password"] == "***" + assert state["_token"] == "***" + assert state["username"] == "u" + assert state["registry_url"] == "reg.io" + + def test_getstate_none_credentials(self): + adapter = OciRegistryAdapter("reg.io") + state = adapter.__getstate__() + assert state["_password"] is None + assert state["_token"] is None + + def test_repr_redacts_credentials(self): + adapter = OciRegistryAdapter( + "reg.io", username="u", password="s3cret_pw", token="s3cret_tk" + ) + r = repr(adapter) + assert "s3cret_pw" not in r + assert "s3cret_tk" not in r + assert "" in r + + def test_properties_still_work(self): + adapter = OciRegistryAdapter("reg.io", password="secret", token="tok") + assert adapter.password == "secret" + assert adapter.token == "tok" diff --git a/tests/providers/image/lib/registry/test_provider_registry.py b/tests/providers/image/lib/registry/test_provider_registry.py new file mode 100644 index 0000000000..97901ea0c4 --- /dev/null +++ b/tests/providers/image/lib/registry/test_provider_registry.py @@ -0,0 +1,234 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + +from prowler.providers.image.exceptions.exceptions import ( + ImageInvalidFilterError, + ImageMaxImagesExceededError, +) +from prowler.providers.image.image_provider import ImageProvider +from prowler.providers.image.lib.registry.dockerhub_adapter import DockerHubAdapter + +_CLEAN_ENV = { + "PATH": os.environ.get("PATH", ""), + "HOME": os.environ.get("HOME", ""), +} + + +def _build_provider(**overrides): + defaults = dict( + images=[], + registry="myregistry.io", + image_filter=None, + tag_filter=None, + max_images=0, + registry_insecure=False, + registry_list_images=False, + config_content={"image": {}}, + ) + defaults.update(overrides) + with patch.dict(os.environ, _CLEAN_ENV, clear=True): + return ImageProvider(**defaults) + + +class TestRegistryEnumeration: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_enumerate_oci_registry(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app/frontend", "app/backend"] + adapter.list_tags.side_effect = [["latest", "v1.0"], ["latest"]] + mock_factory.return_value = adapter + + provider = _build_provider() + assert "myregistry.io/app/frontend:latest" in provider.images + assert "myregistry.io/app/frontend:v1.0" in provider.images + assert "myregistry.io/app/backend:latest" in provider.images + assert len(provider.images) == 3 + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_image_filter(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["prod/app", "dev/app", "staging/app"] + adapter.list_tags.return_value = ["latest"] + mock_factory.return_value = adapter + + provider = _build_provider(image_filter="^prod/") + assert len(provider.images) == 1 + assert "myregistry.io/prod/app:latest" in provider.images + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_tag_filter(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["myapp"] + adapter.list_tags.return_value = ["latest", "v1.0", "v2.0", "dev-abc123"] + mock_factory.return_value = adapter + + provider = _build_provider(tag_filter=r"^v\d+\.\d+$") + assert len(provider.images) == 2 + assert "myregistry.io/myapp:v1.0" in provider.images + assert "myregistry.io/myapp:v2.0" in provider.images + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_combined_filters(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["prod/app", "dev/app"] + adapter.list_tags.return_value = ["latest", "v1.0"] + mock_factory.return_value = adapter + + provider = _build_provider(image_filter="^prod/", tag_filter="^v") + assert len(provider.images) == 1 + assert "myregistry.io/prod/app:v1.0" in provider.images + + +class TestMaxImages: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_max_images_exceeded(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app1", "app2", "app3"] + adapter.list_tags.return_value = ["latest", "v1.0"] + mock_factory.return_value = adapter + + with pytest.raises(ImageMaxImagesExceededError): + _build_provider(max_images=2) + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_max_images_not_exceeded(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app1"] + adapter.list_tags.return_value = ["latest"] + mock_factory.return_value = adapter + + provider = _build_provider(max_images=10) + assert len(provider.images) == 1 + + +class TestDeduplication: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_deduplication_with_explicit_images(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["myapp"] + adapter.list_tags.return_value = ["latest"] + mock_factory.return_value = adapter + + provider = _build_provider(images=["myregistry.io/myapp:latest"]) + assert provider.images.count("myregistry.io/myapp:latest") == 1 + + +class TestInvalidFilters: + def test_invalid_image_filter_regex(self): + with pytest.raises(ImageInvalidFilterError): + _build_provider(image_filter="[invalid") + + def test_invalid_tag_filter_regex(self): + with pytest.raises(ImageInvalidFilterError): + _build_provider(tag_filter="(unclosed") + + +class TestRegistryInsecure: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_insecure_passes_verify_false(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app"] + adapter.list_tags.return_value = ["latest"] + mock_factory.return_value = adapter + + _build_provider(registry_insecure=True) + mock_factory.assert_called_once() + call_kwargs = mock_factory.call_args[1] + assert call_kwargs["verify_ssl"] is False + + +class TestEmptyRegistry: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_empty_catalog_with_explicit_images(self, mock_factory): + adapter = MagicMock() + adapter.list_repositories.return_value = [] + mock_factory.return_value = adapter + + provider = _build_provider(images=["nginx:latest"]) + assert provider.images == ["nginx:latest"] + + +class TestRegistryList: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_list_prints_and_returns(self, mock_factory, capsys): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app/frontend", "app/backend"] + adapter.list_tags.side_effect = [["latest", "v1.0"], ["latest"]] + mock_factory.return_value = adapter + + provider = _build_provider(registry_list_images=True) + + assert provider._listing_only is True + captured = capsys.readouterr() + assert "app/frontend" in captured.out + assert "app/backend" in captured.out + assert "latest" in captured.out + assert "v1.0" in captured.out + assert "2 repositories" in captured.out + assert "3 images" in captured.out + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_list_respects_image_filter(self, mock_factory, capsys): + adapter = MagicMock() + adapter.list_repositories.return_value = ["prod/app", "dev/app"] + adapter.list_tags.return_value = ["latest"] + mock_factory.return_value = adapter + + provider = _build_provider(registry_list_images=True, image_filter="^prod/") + + assert provider._listing_only is True + captured = capsys.readouterr() + assert "prod/app" in captured.out + assert "dev/app" not in captured.out + assert "1 repository" in captured.out + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_list_respects_tag_filter(self, mock_factory, capsys): + adapter = MagicMock() + adapter.list_repositories.return_value = ["myapp"] + adapter.list_tags.return_value = ["latest", "v1.0", "dev-abc"] + mock_factory.return_value = adapter + + provider = _build_provider(registry_list_images=True, tag_filter=r"^v\d+\.\d+$") + + assert provider._listing_only is True + captured = capsys.readouterr() + assert "v1.0" in captured.out + assert "dev-abc" not in captured.out + assert "1 image)" in captured.out + + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_registry_list_skips_max_images(self, mock_factory, capsys): + adapter = MagicMock() + adapter.list_repositories.return_value = ["app1", "app2", "app3"] + adapter.list_tags.return_value = ["latest", "v1.0"] + mock_factory.return_value = adapter + + # max_images=1 would normally raise, but --registry-list skips it + provider = _build_provider(registry_list_images=True, max_images=1) + + assert provider._listing_only is True + captured = capsys.readouterr() + assert "6 images" in captured.out + + +class TestDockerHubEnumeration: + @patch("prowler.providers.image.image_provider.create_registry_adapter") + def test_dockerhub_images_use_repo_tag_format(self, mock_factory): + """Docker Hub images should use repo:tag format without host prefix.""" + adapter = MagicMock(spec=DockerHubAdapter) + adapter.list_repositories.return_value = ["myorg/app1", "myorg/app2"] + adapter.list_tags.side_effect = [["latest", "v1.0"], ["latest"]] + mock_factory.return_value = adapter + + provider = _build_provider(registry="docker.io/myorg") + # Docker Hub images should NOT have host prefix + assert "myorg/app1:latest" in provider.images + assert "myorg/app1:v1.0" in provider.images + assert "myorg/app2:latest" in provider.images + # Ensure no host prefix was added + for img in provider.images: + assert not img.startswith("docker.io/"), f"Unexpected host prefix in {img}" + assert len(provider.images) == 3