diff --git a/.github/workflows/zia-test.yml b/.github/workflows/zia-test.yml index 67f42e70..75fa9ff6 100644 --- a/.github/workflows/zia-test.yml +++ b/.github/workflows/zia-test.yml @@ -94,7 +94,7 @@ jobs: - ZIA_ZSCLOUD - ZIA_ZS0 - ZIA_ZS1 - - ZIA_ZS2 + # - ZIA_ZS2 - ZIA_ZS3 environment: ${{ matrix.environment }} steps: diff --git a/docsrc/zs/zpa/idp.rst b/docsrc/zs/zpa/idp.rst index 03b7db99..579f7046 100644 --- a/docsrc/zs/zpa/idp.rst +++ b/docsrc/zs/zpa/idp.rst @@ -1,5 +1,5 @@ -IDP Controller API -------------------- +idp +--------------- The following methods allow for interaction with the ZPA IDP Controller API endpoints. diff --git a/docsrc/zs/zpa/isolation.rst b/docsrc/zs/zpa/isolation.rst new file mode 100644 index 00000000..9b19ab5d --- /dev/null +++ b/docsrc/zs/zpa/isolation.rst @@ -0,0 +1,14 @@ +isolation +---------- + +The following methods allow for interaction with the ZPA +Cloud Browser Isolation Controller API endpoints. + +Methods are accessible via ``zpa.isolation`` + +.. _zpa-isolation: + +.. automodule:: zscaler.zpa.isolation + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docsrc/zs/zpa/isolation_profile.rst b/docsrc/zs/zpa/isolation_profile.rst deleted file mode 100644 index 6770b05e..00000000 --- a/docsrc/zs/zpa/isolation_profile.rst +++ /dev/null @@ -1,14 +0,0 @@ -isolation_profile ------------------ - -The following methods allow for interaction with the ZPA -Cloud Browser Isolation Profile Controller API endpoints. - -Methods are accessible via ``zpa.isolation_profile`` - -.. _zpa-isolation_profile: - -.. automodule:: zscaler.zpa.isolation_profile - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 19d1376d..d49ffb60 100644 --- a/poetry.lock +++ b/poetry.lock @@ -116,6 +116,70 @@ files = [ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -307,6 +371,45 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "3.4.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.6" +files = [ + {file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, + {file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, + {file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, + {file = "cryptography-3.4.8-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d"}, + {file = "cryptography-3.4.8-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89"}, + {file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, + {file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, + {file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, + {file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, + {file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, +] + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + [[package]] name = "docutils" version = "0.20.1" @@ -710,6 +813,17 @@ files = [ {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pycryptodomex" version = "3.20.0" @@ -1602,4 +1716,4 @@ dev = ["aenum", "black", "pydash"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "4d0b43330e9a24528fb7463f670cea5b39fb16b56c4bcf71980b85a4cfefc1fa" +content-hash = "348b6dbce5147d00acd1d2a0d31115354b9e2c4b5c9f08c332723c8c99b9b181" diff --git a/pyproject.toml b/pyproject.toml index 34587b00..1b4c1cbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ pydash = "*" flake8 = "*" pytz = "*" black = "^24.4.1" +cryptography = "^3.4" [tool.poetry.dev-dependencies] black = "*" diff --git a/tests/integration/zpa/test_app_protection_custom_control.py b/tests/integration/zpa/test_app_protection_custom_control.py new file mode 100644 index 00000000..97ad4185 --- /dev/null +++ b/tests/integration/zpa/test_app_protection_custom_control.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestAppProtectionCustomControl: + """ + Integration Tests for the App Protection Custom Control + """ + + def test_app_protection_custom_control(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + + control_name = "tests-" + generate_random_string() + control_id = None # Define control_id here to ensure it's accessible throughout + + try: + # Create a new custom control + created_control = client.inspection.add_custom_control( + name=control_name, + description=control_name, + action="PASS", + default_action="PASS", + paranoia_level="1", + severity="CRITICAL", + type="RESPONSE", + rules=[ + { + "names": ["test"], + "type": "RESPONSE_HEADERS", + "conditions": [ + {"lhs": "SIZE", "op": "GE", "rhs": "1000"}, + ], + }, + { + "type": "RESPONSE_BODY", + "conditions": [{"lhs": "SIZE", "op": "GE", "rhs": "1000"}], + }, + ], + ) + if created_control and "id" in created_control: + control_id = created_control.id + assert control_id is not None # Asserting that a non-null ID is returned + else: + errors.append("Custom control creation failed or returned unexpected data") + + # Assuming control_id is valid and the banner was created successfully + if control_id: + # Update the custom control + updated_name = control_name + " Updated" + client.inspection.update_custom_control(control_id, name=updated_name) + updated_control = client.inspection.get_custom_control(control_id) + assert updated_control.name == updated_name # Verify update by checking the updated attribute + + # List custom controls and ensure the updated banner is in the list + controls_list = client.inspection.list_custom_controls() + assert any(control.id == control_id for control in controls_list) + + except Exception as exc: + errors.append(exc) + + finally: + # Cleanup resources + if control_id: + try: + client.inspection.delete_custom_control(control_id=control_id) + except Exception as exc: + errors.append(f"Deleting custom control failed: {exc}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during the custom control lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_app_protection_list_controls.py b/tests/integration/zpa/test_app_protection_list_controls.py new file mode 100644 index 00000000..101de519 --- /dev/null +++ b/tests/integration/zpa/test_app_protection_list_controls.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest +from tests.integration.zpa.conftest import MockZPAClient + + +@pytest.fixture +def fs(): + yield + + +class TestAppProtectionControls: + """ + Integration Tests for the App Protection Controls + """ + + def test_list_control_action_types(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + try: + action_types = client.inspection.list_control_action_types() + assert len(action_types) > 0, "No action types returned" + print("Action Types:", action_types) + except Exception as exc: + errors.append(f"Failed to list control action types: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_list_control_severity_types(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + try: + severity_types = client.inspection.list_control_severity_types() + assert len(severity_types) > 0, "No severity types returned" + print("Severity Types:", severity_types) + except Exception as exc: + errors.append(f"Failed to list control severity types: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_list_control_types(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + try: + control_types = client.inspection.list_control_types() + assert len(control_types) > 0, "No control types returned" + print("Control Types:", control_types) + except Exception as exc: + errors.append(f"Failed to list control types: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_list_custom_http_methods(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + try: + http_methods = client.inspection.list_custom_http_methods() + assert len(http_methods) > 0, "No HTTP methods returned" + print("HTTP Methods:", http_methods) + except Exception as exc: + errors.append(f"Failed to list HTTP methods: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_list_predef_control_versions(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + try: + versions = client.inspection.list_predef_control_versions() + assert len(versions) > 0, "No versions returned" + print("Versions:", versions) + except Exception as exc: + errors.append(f"Failed to list versions: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_list_predef_controls(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + version = "OWASP_CRS/3.3.0" # Example version for the test + + try: + # Fetch predefined controls without search term + predef_controls = client.inspection.list_predef_controls(version=version) + assert len(predef_controls) > 0, "No predefined controls returned for version" + print("Predefined Controls for Version:", predef_controls) + + # Fetch predefined controls with search term + predef_controls_with_search = client.inspection.list_predef_controls(version=version) + assert len(predef_controls_with_search) > 0, "No predefined controls returned for search" + print("Predefined Controls for Search Term:", predef_controls_with_search) + + except Exception as exc: + errors.append(f"Failed to list predefined controls: {exc}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred: {errors}" diff --git a/tests/integration/zpa/test_app_protection_profile.py b/tests/integration/zpa/test_app_protection_profile.py new file mode 100644 index 00000000..e99a6e3e --- /dev/null +++ b/tests/integration/zpa/test_app_protection_profile.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestAppProtectionProfile: + """ + Integration Tests for the App Protection Profile + """ + + def test_app_protection_profile(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + + profile_name = "tests-" + generate_random_string() + profile_id = None # Define profile_id here to ensure it's accessible throughout + + try: + # Create a new app protection security profile + created_profile = client.inspection.add_profile( + name=profile_name, + paranoia_level=2, + predef_controls_version="OWASP_CRS/3.3.0", + incarnation_number="6", + ) + if created_profile and "id" in created_profile: + profile_id = created_profile.id + assert profile_id is not None # Asserting that a non-null ID is returned + else: + errors.append("App protection security profile creation failed or returned unexpected data") + + # Assuming profile_id is valid and the banner was created successfully + if profile_id: + # Update the app protection security profile + updated_name = profile_name + " Updated" + client.inspection.update_profile(profile_id, name=updated_name) + updated_profile = client.inspection.get_profile(profile_id) + assert updated_profile.name == updated_name # Verify update by checking the updated attribute + + # List app protection security profiles and ensure the updated banner is in the list + profiles_list = client.inspection.list_profiles() + assert any(profile.id == profile_id for profile in profiles_list) + + except Exception as exc: + errors.append(exc) + + finally: + # Cleanup resources + if profile_id: + try: + client.inspection.delete_profile(profile_id=profile_id) + except Exception as exc: + errors.append(f"Deleting app protection security profile failed: {exc}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during the app protection security profile lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_application_segment.py b/tests/integration/zpa/test_application_segment.py index b0d6942a..28b8bce5 100644 --- a/tests/integration/zpa/test_application_segment.py +++ b/tests/integration/zpa/test_application_segment.py @@ -42,114 +42,115 @@ def test_application_segment(self, fs): segment_group_id = None server_group_id = None app_segment_id = None - # Create an App Connector Group - try: - app_connector_group_name = "tests-" + generate_random_string() - app_connector_group_description = "tests-" + generate_random_string() - created_app_connector_group = client.connectors.add_connector_group( - name=app_connector_group_name, - description=app_connector_group_description, - enabled=True, - latitude="37.33874", - longitude="-121.8852525", - location="San Jose, CA, USA", - upgrade_day="SUNDAY", - upgrade_time_in_secs="66600", - override_version_profile=True, - version_profile_name="Default", - version_profile_id="0", - dns_query_type="IPV4_IPV6", - pra_enabled=True, - tcp_quick_ack_app=True, - tcp_quick_ack_assistant=True, - tcp_quick_ack_read_assistant=True, - ) - app_connector_group_id = created_app_connector_group["id"] - except Exception as exc: - errors.append(f"Creating App Connector Group failed: {exc}") - - # Create a Segment Group - try: - segment_group_name = "tests-" + generate_random_string() - created_segment_group = client.segment_groups.add_group(name=segment_group_name, enabled=True) - segment_group_id = created_segment_group["id"] - except Exception as exc: - errors.append(f"Creating Segment Group failed: {exc}") - # Create a Server Group - try: - server_group_name = "tests-" + generate_random_string() - server_group_description = "tests-" + generate_random_string() - created_server_group = client.server_groups.add_group( - name=server_group_name, - description=server_group_description, - enabled=True, - dynamic_discovery=True, - app_connector_group_ids=[app_connector_group_id], # List of App Connector Group IDs - ) - server_group_id = created_server_group["id"] - except Exception as exc: - errors.append(f"Creating Server Group failed: {exc}") - - # Create an Application Segment - time.sleep(1) - try: - app_segment_name = "tests-" + generate_random_string() - app_segment_description = "tests-" + generate_random_string() - app_segment = client.app_segments.add_segment( - name=app_segment_name, - description=app_segment_description, - enabled=True, - domain_names=["test.example.com"], - segment_group_id=segment_group_id, - server_group_ids=[server_group_id], - tcp_port_ranges=["8001", "8001"], - ) - app_segment_id = app_segment["id"] - except Exception as exc: - errors.append(f"Creating Application Segment failed: {exc}") - - # Test retrieving the specific Application Segment try: - remote_app = client.app_segments.get_segment(segment_id=app_segment_id) - assert remote_app["id"] == app_segment_id - except Exception as exc: - errors.append(f"Retrieving Application Segment failed: {exc}") + # Create an App Connector Group + try: + app_connector_group_name = "tests-" + generate_random_string() + app_connector_group_description = "tests-" + generate_random_string() + created_app_connector_group = client.connectors.add_connector_group( + name=app_connector_group_name, + description=app_connector_group_description, + enabled=True, + latitude="37.33874", + longitude="-121.8852525", + location="San Jose, CA, USA", + upgrade_day="SUNDAY", + upgrade_time_in_secs="66600", + override_version_profile=True, + version_profile_name="Default", + version_profile_id="0", + dns_query_type="IPV4_IPV6", + pra_enabled=True, + tcp_quick_ack_app=True, + tcp_quick_ack_assistant=True, + tcp_quick_ack_read_assistant=True, + ) + app_connector_group_id = created_app_connector_group["id"] + except Exception as exc: + errors.append(f"Creating App Connector Group failed: {exc}") - # Test listing Application Segments - Filter by the unique name - try: - # Since you generate a unique name for the segment, you can use it to search - apps = client.app_segments.list_segments(search=app_segment_name) - assert any(app["id"] == app_segment_id for app in apps), "Newly created app segment should be in the list" - except Exception as exc: - errors.append(f"Listing Application Segments failed: {exc}") + # Create a Segment Group + try: + segment_group_name = "tests-" + generate_random_string() + created_segment_group = client.segment_groups.add_group(name=segment_group_name, enabled=True) + segment_group_id = created_segment_group["id"] + except Exception as exc: + errors.append(f"Creating Segment Group failed: {exc}") - # Test updating the Application Segment - try: - updated_description = "Updated " + generate_random_string() - client.app_segments.update_segment(segment_id=app_segment_id, description=updated_description) - updated_app = client.app_segments.get_segment(segment_id=app_segment_id) - assert updated_app["description"] == updated_description - except Exception as exc: - errors.append(f"Updating Application Segment failed: {exc}") - - # Cleanup resources - if app_segment_id: + # Create a Server Group + try: + server_group_name = "tests-" + generate_random_string() + server_group_description = "tests-" + generate_random_string() + created_server_group = client.server_groups.add_group( + name=server_group_name, + description=server_group_description, + enabled=True, + dynamic_discovery=True, + app_connector_group_ids=[app_connector_group_id], + ) + server_group_id = created_server_group["id"] + except Exception as exc: + errors.append(f"Creating Server Group failed: {exc}") + + # Create an Application Segment + try: + app_segment_name = "tests-" + generate_random_string() + app_segment_description = "tests-" + generate_random_string() + app_segment = client.app_segments.add_segment( + name=app_segment_name, + description=app_segment_description, + enabled=True, + domain_names=["test.example.com"], + segment_group_id=segment_group_id, + server_group_ids=[server_group_id], + tcp_port_ranges=["8001", "8001"], + ) + app_segment_id = app_segment["id"] + except Exception as exc: + errors.append(f"Creating Application Segment failed: {exc}") + + # Test retrieving the specific Application Segment try: - client.app_segments.delete_segment(segment_id=app_segment_id, force_delete=True) + remote_app = client.app_segments.get_segment(segment_id=app_segment_id) + assert remote_app["id"] == app_segment_id except Exception as exc: - errors.append(f"Deleting Application Segment failed: {exc}") + errors.append(f"Retrieving Application Segment failed: {exc}") - if server_group_id: + # Test listing Application Segments - Filter by the unique name try: - client.server_groups.delete_group(group_id=server_group_id) + apps = client.app_segments.list_segments(search=app_segment_name) + assert any(app["id"] == app_segment_id for app in apps), "Newly created app segment should be in the list" except Exception as exc: - errors.append(f"Deleting Server Group failed: {exc}") + errors.append(f"Listing Application Segments failed: {exc}") - if segment_group_id: + # Test updating the Application Segment try: - client.segment_groups.delete_group(group_id=segment_group_id) + updated_description = "Updated " + generate_random_string() + client.app_segments.update_segment(segment_id=app_segment_id, description=updated_description) + updated_app = client.app_segments.get_segment(segment_id=app_segment_id) + assert updated_app["description"] == updated_description except Exception as exc: - errors.append(f"Deleting Segment Group failed: {exc}") + errors.append(f"Updating Application Segment failed: {exc}") + + finally: + # Cleanup resources + if app_segment_id: + try: + client.app_segments.delete_segment(segment_id=app_segment_id, force_delete=True) + except Exception as exc: + errors.append(f"Deleting Application Segment failed: {exc}") + + if server_group_id: + try: + client.server_groups.delete_group(group_id=server_group_id) + except Exception as exc: + errors.append(f"Deleting Server Group failed: {exc}") + + if segment_group_id: + try: + client.segment_groups.delete_group(group_id=segment_group_id) + except Exception as exc: + errors.append(f"Deleting Segment Group failed: {exc}") assert len(errors) == 0, f"Errors occurred during the Application Segment lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_application_segment_inspection.py b/tests/integration/zpa/test_application_segment_inspection.py new file mode 100644 index 00000000..726225da --- /dev/null +++ b/tests/integration/zpa/test_application_segment_inspection.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +import time + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestApplicationSegmentInspection: + """ + Integration Tests for the Applications Segment Inspection + """ + + def test_application_segment_inspection(self, fs): + client = MockZPAClient(fs) + errors = [] + + # Initialize IDs for cleanup + app_connector_group_id = None + segment_group_id = None + server_group_id = None + app_segment_id = None + first_cert_id = None + + try: + # Retrieve the first certificate + try: + certs = client.certificates.list_all_certificates() + assert certs, "Failed to retrieve certificates" + first_cert_id = certs[0]["id"] + except Exception as exc: + errors.append(f"Retrieving certificates failed: {exc}") + + # Create an App Connector Group + try: + app_connector_group_name = "tests-" + generate_random_string() + app_connector_group_description = "tests-" + generate_random_string() + created_app_connector_group = client.connectors.add_connector_group( + name=app_connector_group_name, + description=app_connector_group_description, + enabled=True, + latitude="37.33874", + longitude="-121.8852525", + location="San Jose, CA, USA", + upgrade_day="SUNDAY", + upgrade_time_in_secs="66600", + override_version_profile=True, + version_profile_name="Default", + version_profile_id="0", + dns_query_type="IPV4_IPV6", + pra_enabled=True, + tcp_quick_ack_app=True, + tcp_quick_ack_assistant=True, + tcp_quick_ack_read_assistant=True, + ) + app_connector_group_id = created_app_connector_group["id"] + except Exception as exc: + errors.append(f"Creating App Connector Group failed: {exc}") + + # Create a Segment Group + try: + segment_group_name = "tests-" + generate_random_string() + created_segment_group = client.segment_groups.add_group(name=segment_group_name, enabled=True) + segment_group_id = created_segment_group["id"] + except Exception as exc: + errors.append(f"Creating Segment Group failed: {exc}") + + # Create a Server Group + try: + server_group_name = "tests-" + generate_random_string() + server_group_description = "tests-" + generate_random_string() + created_server_group = client.server_groups.add_group( + name=server_group_name, + description=server_group_description, + enabled=True, + dynamic_discovery=True, + app_connector_group_ids=[app_connector_group_id], + ) + server_group_id = created_server_group["id"] + except Exception as exc: + errors.append(f"Creating Server Group failed: {exc}") + + # Create an Application Segment + try: + app_segment_name = "tests-" + generate_random_string() + app_segment_description = "tests-" + generate_random_string() + app_segment = client.app_segments_inspection.add_segment_inspection( + name=app_segment_name, + description=app_segment_description, + enabled=True, + domain_names=["test" + generate_random_string() + ".example.com"], + segment_group_id=segment_group_id, + server_group_ids=[server_group_id], + tcp_port_ranges=["443", "443"], + common_apps_dto={ + "apps_config": [ + { + "name": app_segment_name, + "description": app_segment_description, + "enabled": True, + "app_types": ["INSPECT"], + "application_port": "443", + "application_protocol": "HTTPS", + "certificate_id": first_cert_id, + "domain": "server1.bd-redhat.com", + } + ] + }, + ) + app_segment_id = app_segment["id"] + except Exception as exc: + errors.append(f"Creating Application Segment failed: {exc}") + + # Test retrieving the specific Application Segment + try: + remote_app = client.app_segments_inspection.get_segment_inspection(segment_id=app_segment_id) + assert remote_app["id"] == app_segment_id + except Exception as exc: + errors.append(f"Retrieving Application Segment failed: {exc}") + + # Test listing Application Segments - Filter by the unique name + try: + apps = client.app_segments_inspection.list_segment_inspection(search=app_segment_name) + assert any(app["id"] == app_segment_id for app in apps), "Newly created app segment should be in the list" + except Exception as exc: + errors.append(f"Listing Application Segments failed: {exc}") + + # Test updating the Application Segment + try: + updated_description = "Updated " + generate_random_string() + client.app_segments_inspection.update_segment_inspection( + segment_id=app_segment_id, description=updated_description + ) + updated_app = client.app_segments_inspection.get_segment_inspection(segment_id=app_segment_id) + assert updated_app["description"] == updated_description + except Exception as exc: + errors.append(f"Updating Application Segment failed: {exc}") + + finally: + # Cleanup resources + cleanup_errors = [] + if app_segment_id: + try: + client.app_segments_inspection.delete_segment_inspection(segment_id=app_segment_id, force_delete=True) + except Exception as exc: + cleanup_errors.append(f"Deleting Application Segment failed: {exc}") + if server_group_id: + try: + client.server_groups.delete_group(group_id=server_group_id) + except Exception as exc: + cleanup_errors.append(f"Deleting Server Group failed: {exc}") + if segment_group_id: + try: + client.segment_groups.delete_group(group_id=segment_group_id) + except Exception as exc: + cleanup_errors.append(f"Deleting Segment Group failed: {exc}") + if cleanup_errors: + errors.extend(cleanup_errors) + + assert len(errors) == 0, f"Errors occurred during the Application Segment lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_application_segment_pra.py b/tests/integration/zpa/test_application_segment_pra.py new file mode 100644 index 00000000..5e90d454 --- /dev/null +++ b/tests/integration/zpa/test_application_segment_pra.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +import time + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestApplicationSegmentPRA: + """ + Integration Tests for the Applications Segment PRA + """ + + def test_application_segment_pra(self, fs): + client = MockZPAClient(fs) + errors = [] + + # Initialize IDs for cleanup + app_connector_group_id = None + segment_group_id = None + server_group_id = None + app_segment_id = None + + try: + # Create an App Connector Group + try: + app_connector_group_name = "tests-" + generate_random_string() + app_connector_group_description = "tests-" + generate_random_string() + created_app_connector_group = client.connectors.add_connector_group( + name=app_connector_group_name, + description=app_connector_group_description, + enabled=True, + latitude="37.33874", + longitude="-121.8852525", + location="San Jose, CA, USA", + upgrade_day="SUNDAY", + upgrade_time_in_secs="66600", + override_version_profile=True, + version_profile_name="Default", + version_profile_id="0", + dns_query_type="IPV4_IPV6", + pra_enabled=True, + tcp_quick_ack_app=True, + tcp_quick_ack_assistant=True, + tcp_quick_ack_read_assistant=True, + ) + app_connector_group_id = created_app_connector_group["id"] + except Exception as exc: + errors.append(f"Creating App Connector Group failed: {exc}") + + # Create a Segment Group + try: + segment_group_name = "tests-" + generate_random_string() + created_segment_group = client.segment_groups.add_group(name=segment_group_name, enabled=True) + segment_group_id = created_segment_group["id"] + except Exception as exc: + errors.append(f"Creating Segment Group failed: {exc}") + + # Create a Server Group + try: + server_group_name = "tests-" + generate_random_string() + server_group_description = "tests-" + generate_random_string() + created_server_group = client.server_groups.add_group( + name=server_group_name, + description=server_group_description, + enabled=True, + dynamic_discovery=True, + app_connector_group_ids=[app_connector_group_id], + ) + server_group_id = created_server_group["id"] + except Exception as exc: + errors.append(f"Creating Server Group failed: {exc}") + + # Create an Application Segment + try: + app_segment_name = "tests-" + generate_random_string() + app_segment_description = "tests-" + generate_random_string() + app_segment = client.app_segments_pra.add_segment_pra( + name=app_segment_name, + description=app_segment_description, + enabled=True, + domain_names=["test" + generate_random_string() + ".example.com"], + segment_group_id=segment_group_id, + server_group_ids=[server_group_id], + tcp_port_ranges=["22", "3389"], + common_apps_dto={ + "apps_config": [ + { + "name": "ssh" + app_segment_name, + "description": "ssh" + app_segment_description, + "enabled": True, + "app_types": ["SECURE_REMOTE_ACCESS"], + "application_port": "22", + "application_protocol": "SSH", + "domain": "ssh_pra.bd-redhat.com", + }, + { + "name": "rdp" + app_segment_name, + "description": "rdp" + app_segment_description, + "enabled": True, + "app_types": ["SECURE_REMOTE_ACCESS"], + "application_port": "3389", + "application_protocol": "RDP", + "connection_security": "ANY", + "domain": "rdp_pra.bd-redhat.com", + }, + ] + }, + ) + app_segment_id = app_segment["id"] + except Exception as exc: + errors.append(f"Creating Application Segment failed: {exc}") + + # Test retrieving, listing, and updating operations in the same try block for readability + try: + # Retrieve specific Application Segment + remote_app = client.app_segments_pra.get_segment_pra(segment_id=app_segment_id) + assert remote_app["id"] == app_segment_id + + # List Application Segments + apps = client.app_segments_pra.list_segments_pra(search=app_segment_name) + assert any(app["id"] == app_segment_id for app in apps), "Newly created app segment should be in the list" + + # Update the Application Segment + updated_description = "Updated " + generate_random_string() + client.app_segments_pra.update_segment_pra(segment_id=app_segment_id, description=updated_description) + updated_app = client.app_segments_pra.get_segment_pra(segment_id=app_segment_id) + assert updated_app["description"] == updated_description + except Exception as exc: + errors.append(f"Operation on Application Segment failed: {exc}") + + finally: + # Cleanup resources + if app_segment_id: + try: + client.app_segments_pra.delete_segment_pra(segment_id=app_segment_id, force_delete=True) + except Exception as exc: + errors.append(f"Deleting Application Segment failed: {exc}") + if server_group_id: + try: + client.server_groups.delete_group(group_id=server_group_id) + except Exception as exc: + errors.append(f"Deleting Server Group failed: {exc}") + if segment_group_id: + try: + client.segment_groups.delete_group(group_id=segment_group_id) + except Exception as exc: + errors.append(f"Deleting Segment Group failed: {exc}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during the Application Segment lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_ba_certificates.py b/tests/integration/zpa/test_ba_certificates.py index 94fe7515..af2aaf9a 100644 --- a/tests/integration/zpa/test_ba_certificates.py +++ b/tests/integration/zpa/test_ba_certificates.py @@ -15,9 +15,16 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +import cryptography.hazmat.backends as crypto_backends +import cryptography.hazmat.primitives.asymmetric.rsa as rsa +import cryptography.hazmat.primitives.serialization as serialization +from cryptography.hazmat.primitives import hashes +from cryptography import x509 +from cryptography.x509.oid import NameOID +import datetime import pytest - from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string @pytest.fixture @@ -25,12 +32,105 @@ def fs(): yield -class TestBaCertificate: +class TestBACertificates: """ - Integration Tests for the certificates. + Integration Tests for the BA Certificate """ + def generate_root_certificate(self, name): + # Generate private key for root certificate + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=crypto_backends.default_backend()) + + # Build the certificate + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "San Jose"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "BD-RedHat"), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "IT Department"), + x509.NameAttribute(NameOID.COMMON_NAME, "bd-redhat.com"), + ] + ) + certificate = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(private_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650)) # Valid for 10 years + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .sign(private_key, hashes.SHA256(), crypto_backends.default_backend()) + ) + + # Convert to PEM format + pem = certificate.public_bytes(serialization.Encoding.PEM) + key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + return pem + key_pem # Combine both PEMs into one to simulate merged key and cert + def test_ba_certificate(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + cert_id = None # Initialize cert_id + + cert_name = "tests-" + generate_random_string() + + try: + # Generate a root certificate + cert_blob = self.generate_root_certificate(cert_name) + + # Create a new certificate + try: + created_cert = client.certificates.add_certificate(name=cert_name, cert_blob=cert_blob.decode("utf-8")) + cert_id = created_cert.id if created_cert and "id" in created_cert else None + if not cert_id: + errors.append("Failed to create certificate") + except Exception as exc: + errors.append(f"Certificate creation failed: {str(exc)}") + + # Retrieve the specific certificate + try: + retrieved_cert = client.certificates.get_certificate(cert_id) + assert retrieved_cert.id == cert_id, "Retrieved certificate ID does not match" + except Exception as exc: + errors.append(f"Retrieving certificate failed: {str(exc)}") + + # List all issued certificates and verify the created certificate is listed + try: + all_certs = client.certificates.list_issued_certificates() + assert any(cert.id == cert_id for cert in all_certs), "Certificate not found in issued list" + except Exception as exc: + errors.append(f"Listing issued certificates failed: {str(exc)}") + + except Exception as exc: + errors.append(f"Error during certificate management: {str(exc)}") + + finally: + # Attempt to delete the certificate if it was created + if cert_id: + try: + delete_response = client.certificates.delete_certificate(cert_id) + if delete_response != 204: + errors.append(f"Failed to delete certificate, expected 204 status code, received {delete_response}") + except Exception as exc: + errors.append(f"Certificate deletion failed: {str(exc)}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during the test: {errors}" + + +class TestBaListCertificate: + """ + Integration Tests for the list certificates. + """ + + def test_ba_list_certificate(self, fs): client = MockZPAClient(fs) errors = [] # Initialize an empty list to collect errors certificate_id = None diff --git a/tests/integration/zpa/test_isolation_banner.py b/tests/integration/zpa/test_isolation_banner.py new file mode 100644 index 00000000..ca660e98 --- /dev/null +++ b/tests/integration/zpa/test_isolation_banner.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestCBIBanners: + """ + Integration Tests for the Cloud Browser Isolation Banner + """ + + def test_cbi_banner(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + + banner_name = "tests-" + generate_random_string() + banner_id = None # Define banner_id here to ensure it's accessible throughout + + try: + # Create a new isolation banner + created_banner = client.isolation.add_banner( + name=banner_name, + logo="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYQAAABQCAMAAAAuu/JsAAADAFBMVEUAAAAAgL8Ad8MAdr8Ad78Ad78Ad78Adr4Ad74Adr4Ad74Adr8Adr4Ad74Ad74Adr4Adr4Ad74Ad78AeL8Ad8AAd8AAeb4AgL8AktsAesAAd74Adr8Adr4Adr4Adr4Ad78AeMAAgMYAeL8Ad74Adr8Ad74AgP8Ae8EAd8AAdr8Ad8EA//8Ad78Ad74Aeb4Ae8YAdr4Ad74Adr8Ad74Ad78Adr8Ad78Adr4Ad78Adr8Ad78Ad74Ad8EAd78Adr8Ad78AeL4AecAAeckAeL8Ad78Ad74Ad74Ad78AecIAdr8Ad78Ad74Aer4AicQAd74Ad74AecAAi9EAfMEAd8AAeL8Ad78Ad78Ad78Ad74Ad78Ad8QAdsIAgMUAeL8Ad78Ad78AfMEAd8AAd78Ad78Ad78Adr8Ad74Adr8Ad74AeL8AeL4Ad74Adr4Aer8AecAAdr4AeMEAqv8Adr4Adr8Adr8AeL8Ad74Ad74Adr8Adr8Ad78AdsAAd78AeMEAdr8AgL8Aeb8AdsEAd78Adr8Ad78AgMgAd8AAeL4Ad78Adr4Ad78AecIAe78AeL8AgMQAd74Adr8AeMAAdr8AecEAgL8AeL4AmcwAdr8Adr8Ad74Ad78AesgAdr8AeL8AfMQAeL8AjsYAd78Ad78AecMAd78AdsEAeMAAd78Ad78AesIAdr8Ad74Aeb8AeMEAeMMAd8QAd8wAeL8Ad74Ad78AdsAAfMUAd78Adr4AeMMAgNUAgMwAd78Ad8AAd74AdsAAeL4Adr8Ad78Aeb4AesIAeL8Adr8AeL8Ad78AgL8Adr8Ad8EAdr8Ad78Ad74Adr8Ad74Adr4AeL4Ad78Ad78Adr8Ad74Ad78Ad78Ad74AgL8AeMAAdr8Ad8EAd74AeMAAd74Ad78Ad8AAdsAAd74Adr8AesIAdr8Adr8AeL8Ae8UAeb8AeMEAeL8AeMAAd78Ad8EAdsAAdr8AecEAer8Adr4Adr4Ad74Adr8Ad8AAd78Adr8AeL8Ad74Ad8EAeMAAdr5kTMBRAAABAHRSTlMABC9UeJy/0d3p9f78+fHl2My7oIVpPxAHQX689P/go1kSSPC3SwIdjeteAfvJNxum/X966tPIsa7C16Ut1s+QZj0TaPfknVgVqPOhQw29wV0LIU13j6yrnnwrNhZg7pglgcq2hxxH+uxAU9W1LDnNQgNu8mxXdpbGy6qVb0aICExS5rPbDkmGw/jOKjRbGsXnVeJOFDMF9u9PXBdfcydECZ+DJrQpcbq4LpeOKDEiHg+E6NJ9I7LtEQYKx2VyeWpwZzsZk4yAdBh7Vpun4a+SioLjlMDZPN/QDFE4Ppo1uYttRanEMqTeIB9QYiR1sFph2kow3JmtY4lrvmTUOpGi/bwlaAAADvJJREFUeAHs1YPZA2EQAOH9GdvYXKyLbdtW/4WkgBhfHu1bxAw87+v75/fvXyAUiSVSmViuUKrUGq1ObwDyGUaT2WLF82yc3QGMkS+nxoVXSd0eL7BDfP4A3kEY5IEN8s2F8E7hCBAGojF8QDzx9iYRY9KK90mlM5ZsLl8ovvnQpFTG2ySVaq3eAMJGs4W3tDvdHhBmjH28YTAc0QKYGk/wuulsDuTd+MVyte5strvq/udwbN8u4KO4tjCAf8Fhg0uCfLwXJC3dIIu7Bgs1PLziluBs8yiS8lLS4m6BkjYEa3EiWHBIU/didbe0uNb724TcmTs7MzubzTOa/0+T7I7PPeeec1O/LM04+j2CfHlt4KDug2lV6JChuPsNG+4yYuQo/EfYR/eLoGWOLvXxV1CG2SLH4N/PPnYcvTB+Av4SOjJHmWD8u02cRC+UrYy/iN4UJsNXzmpZCT2acQjwKDtD0rOdg9ZF/TMYLoGPTHls6viHpk2f9OCM6MdnBuEu9C8KBeGrmCdm2fo9MRGxYU/igcZhzaE24Sl6oXl9ALPnzJ03n5IFHXD3aUFhIXy3iIuBJezfFsOWLisEleXVaN2KlcCq9uX8Kas5dTXuQnFrmGOtHb6ryKHA01zXOD7sGWl8e5beCE9YPyOCMluRdYm4K9WjMAJ5YMNgO7AxdpOt8OZ1ttkQBtBHERu24G71HIUByAPjngewdUSMzX/b9jYQCtE3YTtGQZc9aOeuCQm79yTh/1g/CpXhuwccyQAi5yLlb0gtjxxTbPRFnb1B0LLvG7txf9vSYsw6MLzBxuUH8X8pljn8e8J3wUlpAJLS8EAcZgeKMS+UPqhW5RBk9l0DHoqiDv/DDevZkXe6jT5ytMrcY8cXV5pt+rE5D59I3jHg5KxTMTCUXvvkY3NfyHjxpfb3p0PSi8IkaLxcuWCnVxoef7XyUPjmgWLMPcdrL0MSOKtLLE00uS8OBkYV9iQZQtrTr79Bwday8CnosY8ZucDGHCU2H0+Hu5gpb8ZSZe3+h2dDWEzhLajErX+7KIXq23sHwszAmYOee+eF8K7QtYG5t7U1JAn3RtKTd48egq519KS/uF/vHaBWk67QCp7ShBprBgRCFh9dk27mVxCp3lQKXSGknaxOjWK9YWBoo2bvMtty6KnBXKs+B2rOrqdpyZnV0DOVnhxFlqCMUOrpfhCSsW2oo9hZqMQdLUVd51oj23kK94i7W3EFdYx/Ge6WPFuWgiMIOvzaMJds0e9D7QPrW4roEwx35+nJh3D5qA4NnA+EIuZj6gv7BMKSwzQS2+tOMqO8w7hj1afUVzQAGgGfzafKcOj5nLn0VCWoTa5Lb1TtCa24EHowPw0A2kfQUDsI+9rSSOROUUFYRmPz4PIFhS+R7VRnGvkqEWpxJw5QkgwdabHMFduORKjEN6OXtsZBwxlCD5YCwEmasP0Dd3xdk8baItvOSJo5CwANKTyKLF1DaOxtqKz6ihofQcczzJUDteWRqA699o0dGt/QgwwAlUNoZgeyDS1OM0/DpeMbNBUNAM9TWASX0RE0EZUEYeG31AiLg46izI3p30Fl9gZKoiY92WPHKy8VeqXKvf3rxtpo4DNobClOc7OAbgeoVjLSQUljZFlyjqY+hUsrmisHIFiJ2yX8AKBeNZrqgxwza1LrSegowNyYmgaV788os7GihTudjYekY2ZfkanLHoHGkgbf2mgiXcqglvWt1BNI21ewKFVWAYDfAqqELH3t7anjw6jiPxBAbSpC+z06ucCEyT+Er6CipXyFigDAkkiqhBYpH/1lGzlW4o5P5tPN59BRgd6LeAZqC+9EKf+6c+8/BH0/ZkTSXQt4EtxOGsCALUr1PDVdfKoKNUNGIyoiCwXAJfDCfCqmAKhLYbuyscfkAzxOoQIAXKSizKAYuGzZTJUkZBn4LmWOb78asQ86ztBrUTOh9uGBrF/OuBQAMzGXY41GZmMx/alSwQ4MYQ7bJijepLAewKZQClWV1D1Tjh1nKTSEyhXmaCUHqqsACqq3IRJiew8qKsHFT54zVU+ulAh9S+i1Zbug9kg1MrR/ZiA8Sppqo0ZLJ8wEzaMi4gcAzsbKxYXKUKXtcg2wP0ShRQwUqt9vhiEltd0LQImttoPAwWUUCkERUIrC43B5myrXM4NhaCy9daYXsol37vzxJFgz8Rw1ZsLEdy2pqHk/AKw3Gss6lFHFjdoUrgQYnO5wGFltY477gXQKbQBspHADav0pzAWA70tSMSQQJj6jl8rEQ82v6s1TsK5+G8q6wNjXy6g4t9Ct95QRJ5fOF32SJRioKg9OCvv7wiEY6FVUid7vS/ftNSCxjvJwy1sYoJmPFaaiAkztp3euL4Gk0hZ4peN0SorDUI0wKobH69Qarx8PgIeM70t4Kf6lmhQWALiXwmWgPYWHITlBYRiAH0M8jnxCW3ql+h746IEylOyGgVshVDRPRLbmlESUe+kTP2iVp7ANXrhn5YtbQ6jyHoCmFBJgVwbIFCckhShUAfA3CrHdYO4AvRG7Gz7rTUlX6BtGlXtFVAunmzUtbo/xg0rP+XLxx7PET47c7j4pjFp/B2KUob2OlE/dgOwtqdT7o4M5bOvhgT+9sGYR8kATqh2DHr9wKvwrelwTMrjV08p9WElhMTzb9so0f+r7HuhAYT8wklZclZ6hN+GJg9bZCiIvzKD8xut4fysVYXOg+M5GA7FzX0a2G8wRkgQPEn+YTkOlAfSRyhGptOI74CkKE+EJvfAKfGCUkG2Au/QFVHQuALVoGgqrkgiXdy3PyP06LaOJGQCaUeiAPbSihB0TKHwLj0Jp2ZfIG62odgNutlSnom19SHqWpbG6AwFso9AIpq6doakTAJSUNCIRnWjFJKlH0xcePUWrvp0NHxju8ja0OgymYvz70JhdlcZS44BbHmr3wqU1dDP4RiUlPn8CbKLQ1GozvrCUxGXCowdpUUg95I0ESm5BY/l8KqKdcGN/OJaGCgEZFA7CRKaNsohpFb5IwzUlD4kDLkvFprq04jLQmMJAePQaLSqEPPIpJV9DdtRGwfEcdCUOakoDV4DNUkfY0JYoKQY3eO5UIlx+Yo7T8vUpCETRgpB4DJXq2h7dR2umBcNU0Orljw3pP/7nw0Un/dw8vG+f3j8afGEKJcvsxpXrEpkw1HpAXRv17EEx5vgZJqZRKPnWROUwXpdKQG0opKvjcirMPEOhBzzbQ0siJsBQ4MSjM96lm9Ci3/zyPbRqh5o112L6UZHyCUzFX5qaaqPW6kAl624GY3MobN4Hhb24Ot0PUnZwDhhNoTnM3KBwEhZcoRUVYGDgrf2hNHYlOjMJiqB2NkqiVkEl6Csqfv0engXMCk+hZGJHCq/D2GElpfKDyiJljjgbqC01lF6lEA0zv1JoDQteoQXXY6An6fhSBz3xnzas91A/wHnP2NeqUaMhVL4rQ0W5JFjj90tnqizpRuEiDPViDluCwWrEunLf8Tjwd4s3IcCmFOCDYUF9f3pWEDp+21CCVtnWrNXbz7IHoFidQkVhP7jZ3CLHEKgt6UwhCvEUysDQCbnjL+wOkVKRIhSuSWtSG8BEDQpbYclNetQUbuyzJtFnYYugmFONgu0Y3CUZ/rtMHwr9kE4hZBXUEo9snl40W4cH1U+4wvmV8uV0wE95zKKcUhZzBZJ75hZpeSbL9PeBdyhUgSUL/elJB2g4F4+j7yJGG1SuQ6dAxy4KdQ2H1Es45KDwDlSGjlM9VZHMMRYqfeXQu0he/FJb7iMrnlkjTdINe0rGNtCDrdCY+SvzQMnFBpXrOr9BT0HVV1+GyqEU5Q9JUlYZtRPC6LUUTh2k0AlC8A4KjtYAHpcbNUuoaJKku2JlRSLgDFMC4gOw5uVImqsEyZbxzAtXVkOI20DFu58c1BPXlYpmfhCSmlP4XVObOncW2a6FOyjcQD29XvM/DlOT33+paYYXp6LsTmRb+CQF2xz5lW0Lq5bT1GGopW2MYF7o8T6E4E/pWeuZVBk3Kw5ZBh4vRmFwkLblYJv24i9dT2Q0ocqCGIyholYCAARlPmmjovhAAFAivmM2ANSiSsSIo+suvfSCtOlkec7NaFjWg2Y+gEq9ccwLtc5CZRs9qxn8NSUH9n+W3Lf7YX9t2RP1S9LU2t3ALqrYUiadf8NGyROamWxRMd03U9WpKRMvhmWBS2mssxOCM9lB383v/zUk7enZVjhT6MGXdrh0oZnBiwB8T2NKv68rhe1wcV6nmRbvw+Wc1N6xrJvJxodBGDWPPltWOLOnd6mByPV+p7lpMciS4KCxyF0WFh4eRZYX3JZWdKKJn3vCJZ1CLLwRX5YGHPco+cUB+qTayB86vJzbpZjrgd01aeahVRaqAEV3IktDGrMdR7ZJbotC/LbS0GuJbllcP3il53jqK4Icj/rTN49DXxDNiVwvkyYy/Cy8Wu/FIFtSGRq5sh7ZeionnII7ZqcaxaxXcccO+Y3yhvNj6qqIbH436CPDanhteiByvcvVaCD1KlTiZlBP2fUQNhU3CFgNA3HHZL2i6Z7z1OEfrgwYTSnUg7eOhehtvRuyBF6kj0pugYFkeiIWZSQspZ6ir9ohmxVLrdSCdqh06093sW/vhrCXwrMQ/I6FUiPiy50QVAuVQtPgtdWpdDMNWd5/kL464VOL9ZI4yNcGUxKx9MImuAvoc9hGxYrPrkFrzEUHVfx/3TDHCZVaBstWfpzbhoqQeZ06QuUshXnIhbiXDlBjL1wSW9BXU5Fndi9PHtJg80PPN+8RvXfsljgYSb/84tSbD1VtXn7j06Og6+WCO77cvHXGaxl7H73vw0RYt6l9w+h+5S7+8cI/xdfyTkByJAUxqqVtpa/GO5HPssQfvnJQKBEHAA3oqwXvI59X4n8J/zUkK7E/nCEClE9+TkI+79lX7dyXHgwXexH6qEEa8vno/Xn0yW078vks8A/mXunayJcn1tVhLvUIQL48ktToDebCU08gX17aVLFf2W/X2kq+W3b8xZK0os1iJ/4N8gXDJf6dxvRkQcFg/Fvli/llnj+NpVbZgv+AfAHLw2N186HNjRLwn5Pv5UpHMvY3bfJt4xL+NasXfWh/+cdmdkM+r/wJofdoV8ItCHgAAAAASUVORK5CYII=", + primary_color="#0076BE", + text_color="#FFFFFF", + banner=True, + notification_title="Heads up, you’ve been redirected to Browser Isolation!", + notification_text="The website you were trying to access is now rendered in a fully isolated environment to protect you from malicious content.", + ) + if created_banner and "id" in created_banner: + banner_id = created_banner.id + assert banner_id is not None # Asserting that a non-null ID is returned + else: + errors.append("Banner creation failed or returned unexpected data") + + # Assuming banner_id is valid and the banner was created successfully + if banner_id: + # Update the isolation banner + updated_name = banner_name + " Updated" + client.isolation.update_banner(banner_id, name=updated_name) + updated_banner = client.isolation.get_banner(banner_id) + assert updated_banner.name == updated_name # Verify update by checking the updated attribute + + # List isolation banners and ensure the updated banner is in the list + banners_list = client.isolation.list_banners() + assert any(banner.id == banner_id for banner in banners_list) + + except Exception as exc: + errors.append(exc) + + finally: + # Attempt to delete the isolation banner if it was created + if banner_id: + try: + # Delete the isolation banner + delete_response_code = client.isolation.delete_banner(banner_id) + assert str(delete_response_code) == "200" # Adjust to expect '200' as per API behavior + except Exception as exc: + errors.append(exc) + + # Assert that no errors occurred during the test + assert len(errors) == 0, f"Errors occurred during the isolation banner lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_isolation_certificate.py b/tests/integration/zpa/test_isolation_certificate.py new file mode 100644 index 00000000..56cda6e3 --- /dev/null +++ b/tests/integration/zpa/test_isolation_certificate.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import cryptography.hazmat.backends as crypto_backends +import cryptography.hazmat.primitives.asymmetric.rsa as rsa +import cryptography.hazmat.primitives.serialization as serialization +from cryptography.hazmat.primitives import hashes +from cryptography import x509 +from cryptography.x509.oid import NameOID +import datetime +import pytest +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestCBICertificates: + """ + Integration Tests for the Cloud Browser Isolation Certificate + """ + + def generate_root_certificate(self, name): + # Generate private key for root certificate + private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=crypto_backends.default_backend()) + + # Build the certificate + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "San Jose"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "BD-RedHat"), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "IT Department"), + x509.NameAttribute(NameOID.COMMON_NAME, "bd-redhat.com"), + ] + ) + certificate = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(private_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after( + # Valid for 10 years + datetime.datetime.utcnow() + + datetime.timedelta(days=3650) + ) + .add_extension( + x509.BasicConstraints(ca=True, path_length=None), + critical=True, + ) + .sign(private_key, hashes.SHA256(), crypto_backends.default_backend()) + ) + + # Convert to PEM format + pem = certificate.public_bytes(serialization.Encoding.PEM) + key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + return pem, key_pem + + def test_cbi_certificate(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + cert_id = None # Initialize cert_id + + cert_name = "tests-" + generate_random_string() + + try: + # Generate a root certificate + pem, _ = self.generate_root_certificate(cert_name) + + try: + # Create a new certificate + created_cert = client.isolation.add_certificate(name=cert_name, pem=pem.decode("utf-8")) + cert_id = created_cert.id if created_cert and "id" in created_cert else None + if not cert_id: + errors.append("Failed to create certificate") + except Exception as exc: + errors.append(f"Certificate creation failed: {str(exc)}") + + if cert_id: + try: + # Update the certificate + updated_name = cert_name + " Updated" + client.isolation.update_certificate(cert_id, name=updated_name) + updated_cert = client.isolation.get_certificate(cert_id) + if updated_cert.name != updated_name: + errors.append("Failed to update certificate name") + except Exception as exc: + errors.append(f"Certificate update failed: {str(exc)}") + + try: + # Verify the certificate by listing + certs = client.isolation.list_certificates() + if cert_id not in [cert.id for cert in certs]: + errors.append("Certificate not found in list") + except Exception as exc: + errors.append(f"Listing certificates failed: {str(exc)}") + + except Exception as exc: + errors.append(f"Error during certificate management: {str(exc)}") + + finally: + # Attempt to delete the certificate if it was created + if cert_id: + try: + # Delete the certificate + delete_response_code = client.isolation.delete_certificate(cert_id) + if str(delete_response_code) != "200": + errors.append(f"Failed to delete certificate, status code {delete_response_code}") + except Exception as exc: + errors.append(f"Certificate deletion failed: {str(exc)}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during the test: {errors}" diff --git a/tests/integration/zpa/test_isolation_profile.py b/tests/integration/zpa/test_isolation_profile.py index 38a3743f..e1c388ca 100644 --- a/tests/integration/zpa/test_isolation_profile.py +++ b/tests/integration/zpa/test_isolation_profile.py @@ -19,7 +19,7 @@ def test_isolation_profile(self, fs): # Attempt to list all isolation profiles try: - isolation_profiles = client.isolation_profile.list_profiles() + isolation_profiles = client.isolation.list_profiles() assert isinstance(isolation_profiles, list), "Expected a list of isolation profiles" except Exception as exc: errors.append(f"Listing isolation profiles failed: {str(exc)}") @@ -31,7 +31,7 @@ def test_isolation_profile(self, fs): # Fetch the selected isolation profile by its ID try: - fetched_profile = client.isolation_profile.get_profile_by_id(profile_id) + fetched_profile = client.isolation.get_profile_by_id(profile_id) assert fetched_profile is not None, "Expected a valid isolation profile object" assert fetched_profile.get("id") == profile_id, "Mismatch in isolation profile ID" except Exception as exc: @@ -40,7 +40,7 @@ def test_isolation_profile(self, fs): # Attempt to retrieve the isolation profile by name try: profile_name = first_profile.get("name") - profile_by_name = client.isolation_profile.get_profile_by_name(profile_name) + profile_by_name = client.isolation.get_profile_by_name(profile_name) assert profile_by_name is not None, "Expected a valid isolation profile object when searching by name" assert profile_by_name.get("id") == profile_id, "Mismatch in isolation profile ID when searching by name" except Exception as exc: diff --git a/tests/integration/zpa/test_isolation_regions.py b/tests/integration/zpa/test_isolation_regions.py new file mode 100644 index 00000000..1d82ee1f --- /dev/null +++ b/tests/integration/zpa/test_isolation_regions.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient + + +@pytest.fixture +def fs(): + yield + + +class TestCBIRegions: + """ + Integration Tests for the CBI Region. + """ + + def test_cbi_region(self, fs): + client = MockZPAClient(fs) # Client instantiation with fixture as specified + errors = [] # Initialize an empty list to collect errors + + # List all CBI regions + try: + regions = client.isolation.list_regions() + assert isinstance(regions, list) and len(regions) >= 2, "Expected at least two CBI regions" + tested_regions = regions[:2] # Test the first two regions + except AssertionError as exc: + errors.append(f"Assertion error: {str(exc)}") + except Exception as exc: + errors.append(f"Listing CBI regions failed: {str(exc)}") + + # Fetch and test each of the selected CBI regions by their IDs + for region in tested_regions: + region_id = region["id"] + try: + fetched_region = client.isolation.get_region(region_id) + assert fetched_region is not None, "Expected a valid CBI region object" + assert fetched_region["id"] == region_id, "Mismatch in CBI region ID" + except AssertionError as exc: + errors.append(f"Assertion error in region {region_id}: {str(exc)}") + except Exception as exc: + errors.append(f"Fetching CBI region by ID {region_id} failed: {str(exc)}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during CBI region operations test: {errors}" diff --git a/tests/integration/zpa/test_isolation_security_profile.py b/tests/integration/zpa/test_isolation_security_profile.py new file mode 100644 index 00000000..bd344d95 --- /dev/null +++ b/tests/integration/zpa/test_isolation_security_profile.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestCBISecurityProfile: + """ + Integration Tests for the Cloud Browser Isolation Security Profile + """ + + def test_cbi_security_profile(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + + profile_name = "tests-" + generate_random_string() + profile_description = "tests-" + generate_random_string() + profile_id = None # Define profile_id here to ensure it's accessible throughout + + try: + try: + # Obtain the cbi certificate + banners = client.isolation.list_banners() + assert banners, "Failed to retrieve CBI banners" + first_banner_id = banners[0]["id"] # Assume banners returns a list of dictionaries with 'id' key + except Exception as exc: + errors.append(f"Retrieving CBI banners failed: {exc}") + + try: + # Obtain the cbi certificate + certs = client.isolation.list_certificates() + assert certs, "Failed to retrieve CBI certificates" + first_cert_id = certs[0]["id"] # Assume certs returns a list of dictionaries with 'id' key + except Exception as exc: + errors.append(f"Retrieving CBI certificates failed: {exc}") + + try: + regions = client.isolation.list_regions() + assert isinstance(regions, list) and len(regions) >= 2, "Expected at least two CBI regions" + tested_regions = [regions[0]["id"], regions[1]["id"]] # Test the first two region IDs + except AssertionError as exc: + errors.append(f"Assertion error: {str(exc)}") + except Exception as exc: + errors.append(f"Listing CBI regions failed: {str(exc)}") + + try: + # Create a new isolation profile + created_profile = client.isolation.add_cbi_profile( + name=profile_name, + description=profile_description, + region_ids=tested_regions, + security_controls={ + "document_viewer": True, + "allow_printing": True, + "watermark": { + "enabled": True, + "show_user_id": True, + "show_timestamp": True, + "show_message": True, + "message": "Test", + }, + "flattened_pdf": False, + "upload_download": "all", + "restrict_keystrokes": True, + "copy_paste": "all", + "local_render": True, + }, + debug_mode={ + "allowed": True, + "file_password": "test-" + generate_random_string(), + }, + banner_id=first_banner_id, + certificate_ids=[first_cert_id], + ) + if created_profile and "id" in created_profile: + profile_id = created_profile["id"] + except Exception as exc: + errors.append(f"Profile creation failed: {exc}") + + try: + # Assuming profile_id is valid and the profile was created successfully + if profile_id: + # Update the isolation profile + updated_name = profile_name + " Updated" + client.isolation.update_cbi_profile(profile_id, name=updated_name) + updated_profile = client.isolation.get_cbi_profile(profile_id) + assert updated_profile["name"] == updated_name # Verify update by checking the updated attribute + + # List isolation profiles and ensure the updated profile is in the list + profiles_list = client.isolation.list_cbi_profiles() + assert any(profile["id"] == profile_id for profile in profiles_list) + + except Exception as exc: + errors.append(exc) + + finally: + # Attempt to delete the isolation profile if it was created + if profile_id: + try: + # Delete the isolation profile + delete_response_code = client.isolation.delete_cbi_profile(profile_id) + assert str(delete_response_code) == "200" # Adjust to expect '200' as per API behavior + except Exception as exc: + errors.append(exc) + + # Assert that no errors occurred during the test + assert len(errors) == 0, f"Errors occurred during the isolation security profile lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_isolation_zpa_profile.py b/tests/integration/zpa/test_isolation_zpa_profile.py new file mode 100644 index 00000000..5b7243da --- /dev/null +++ b/tests/integration/zpa/test_isolation_zpa_profile.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient + + +@pytest.fixture +def fs(): + yield + + +class TestZPACBIProfile: + """ + Integration Tests for the CBI ZPA isolation profile. + """ + + def test_isolation_zpa_profile(self, fs): + client = MockZPAClient(fs) # Assuming the client instantiation is taken care of elsewhere + errors = [] # Initialize an empty list to collect errors + cbi_profile_id = None + + # List all CBI ZPA profiles + try: + profiles = client.isolation.list_zpa_profiles() + assert isinstance(profiles, list) and profiles, "Expected a non-empty list of CBI ZPA profiles" + first_profile = profiles[0] + cbi_profile_id = first_profile.get("cbi_profile_id") + assert cbi_profile_id is not None, "No CBI profile ID found in the first profile" + except AssertionError as exc: + errors.append(f"Assertion error: {str(exc)}") + except Exception as exc: + errors.append(f"Listing CBI ZPA profiles failed: {str(exc)}") + + # Fetch the selected CBI ZPA profile by its ID + if cbi_profile_id: + try: + fetched_profile = client.isolation.get_zpa_profile(cbi_profile_id) + assert fetched_profile is not None, "Expected a valid CBI ZPA profile object" + assert fetched_profile.get("cbi_profile_id") == cbi_profile_id, "Mismatch in CBI ZPA profile ID" + except AssertionError as exc: + errors.append(f"Assertion error: {str(exc)}") + except Exception as exc: + errors.append(f"Fetching CBI ZPA profile by ID failed: {str(exc)}") + + # Assert that no errors occurred during the test + assert not errors, f"Errors occurred during CBI ZPA profile operations test: {errors}" diff --git a/tests/integration/zpa/test_lss_config_controller.py b/tests/integration/zpa/test_lss_config_controller.py new file mode 100644 index 00000000..e3fde3e5 --- /dev/null +++ b/tests/integration/zpa/test_lss_config_controller.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest + +from tests.integration.zpa.conftest import MockZPAClient +from tests.test_utils import generate_random_string + + +@pytest.fixture +def fs(): + yield + + +class TestLSSConfigController: + """ + Integration Tests for the LSS Config Controller + """ + + def test_lss_config_controller(self, fs): + client = MockZPAClient(fs) + errors = [] + + # Initialize IDs for cleanup + lss_config_id = None + app_connector_group_id = None + + try: + try: + # Get IDP by name "BD_Okta_Users" + idp = client.idp.get_idp_by_name("BD_Okta_Users") + if not idp: + raise ValueError("IDP 'BD_Okta_Users' not found") + idp_id = idp["id"] + except Exception as exc: + errors.append(f"Retrieving IdP ID failed: {exc}") + + # Search and get SCIM Group IDs + scim_group_ids = [] + for group_name in ["A000", "B000"]: + try: + groups = client.scim_groups.list_groups(idp_id, search=group_name, sort_order="ASC") + group = next((g for g in groups if g.name == group_name), None) + if group is None: + raise ValueError(f"SCIM Group '{group_name}' not found") + scim_group_ids.append((idp_id, group.id)) + except Exception as exc: + errors.append(f"Searching SCIM Group '{group_name}' failed: {exc}") + + # Create an App Connector Group + try: + app_connector_group_name = "tests-" + generate_random_string() + app_connector_group_description = "tests-" + generate_random_string() + created_app_connector_group = client.connectors.add_connector_group( + name=app_connector_group_name, + description=app_connector_group_description, + enabled=True, + latitude="37.33874", + longitude="-121.8852525", + location="San Jose, CA, USA", + upgrade_day="SUNDAY", + upgrade_time_in_secs="66600", + override_version_profile=True, + version_profile_name="Default", + version_profile_id="0", + dns_query_type="IPV4_IPV6", + pra_enabled=True, + tcp_quick_ack_app=True, + tcp_quick_ack_assistant=True, + tcp_quick_ack_read_assistant=True, + ) + app_connector_group_id = created_app_connector_group["id"] + except Exception as exc: + errors.append(f"Creating App Connector Group failed: {exc}") + + # Create an LSS Config + try: + lss_config_name = "tests-" + generate_random_string() + lss_config_description = "tests-" + generate_random_string() + lss_config = client.lss.add_lss_config( + name=lss_config_name, + description=lss_config_description, + enabled=True, + use_tls=True, + source_log_type="user_activity", + source_log_format="json", + lss_host="192.168.100.1", + lss_port="5000", + audit_message='{"logType":"User Activity","tcpPort":"5000","appConnectorGroups":[{"name":"tests-obuedxxtkw","id":"216199618143358305"}],"domainOrIpAddress":"192.168.100.1","logStreamContent":"{\\"LogTimestamp\\": %j{LogTimestamp:time},\\"Customer\\": %j{Customer},\\"SessionID\\": %j{SessionID},\\"ConnectionID\\": %j{ConnectionID},\\"InternalReason\\": %j{InternalReason},\\"ConnectionStatus\\": %j{ConnectionStatus},\\"IPProtocol\\": %d{IPProtocol},\\"DoubleEncryption\\": %d{DoubleEncryption},\\"Username\\": %j{Username},\\"ServicePort\\": %d{ServicePort},\\"ClientPublicIP\\": %j{ClientPublicIP},\\"ClientPrivateIP\\": %j{ClientPrivateIP},\\"ClientLatitude\\": %f{ClientLatitude},\\"ClientLongitude\\": %f{ClientLongitude},\\"ClientCountryCode\\": %j{ClientCountryCode},\\"ClientZEN\\": %j{ClientZEN},\\"Policy\\": %j{Policy},\\"Connector\\": %j{Connector},\\"ConnectorZEN\\": %j{ConnectorZEN},\\"ConnectorIP\\": %j{ConnectorIP},\\"ConnectorPort\\": %d{ConnectorPort},\\"Host\\": %j{Host},\\"Application\\": %j{Application},\\"AppGroup\\": %j{AppGroup},\\"Server\\": %j{Server},\\"ServerIP\\": %j{ServerIP},\\"ServerPort\\": %d{ServerPort},\\"PolicyProcessingTime\\": %d{PolicyProcessingTime},\\"ServerSetupTime\\": %d{ServerSetupTime},\\"TimestampConnectionStart\\": %j{TimestampConnectionStart:iso8601},\\"TimestampConnectionEnd\\": %j{TimestampConnectionEnd:iso8601},\\"TimestampCATx\\": %j{TimestampCATx:iso8601},\\"TimestampCARx\\": %j{TimestampCARx:iso8601},\\"TimestampAppLearnStart\\": %j{TimestampAppLearnStart:iso8601},\\"TimestampZENFirstRxClient\\": %j{TimestampZENFirstRxClient:iso8601},\\"TimestampZENFirstTxClient\\": %j{TimestampZENFirstTxClient:iso8601},\\"TimestampZENLastRxClient\\": %j{TimestampZENLastRxClient:iso8601},\\"TimestampZENLastTxClient\\": %j{TimestampZENLastTxClient:iso8601},\\"TimestampConnectorZENSetupComplete\\": %j{TimestampConnectorZENSetupComplete:iso8601},\\"TimestampZENFirstRxConnector\\": %j{TimestampZENFirstRxConnector:iso8601},\\"TimestampZENFirstTxConnector\\": %j{TimestampZENFirstTxConnector:iso8601},\\"TimestampZENLastRxConnector\\": %j{TimestampZENLastRxConnector:iso8601},\\"TimestampZENLastTxConnector\\": %j{TimestampZENLastTxConnector:iso8601},\\"ZENTotalBytesRxClient\\": %d{ZENTotalBytesRxClient},\\"ZENBytesRxClient\\": %d{ZENBytesRxClient},\\"ZENTotalBytesTxClient\\": %d{ZENTotalBytesTxClient},\\"ZENBytesTxClient\\": %d{ZENBytesTxClient},\\"ZENTotalBytesRxConnector\\": %d{ZENTotalBytesRxConnector},\\"ZENBytesRxConnector\\": %d{ZENBytesRxConnector},\\"ZENTotalBytesTxConnector\\": %d{ZENTotalBytesTxConnector},\\"ZENBytesTxConnector\\": %d{ZENBytesTxConnector},\\"Idp\\": %j{Idp},\\"ClientToClient\\": %j{c2c},\\"ClientCity\\": %j{ClientCity},\\"MicroTenantID\\": %j{MicroTenantID},\\"AppMicroTenantID\\": %j{AppMicroTenantID},\\"PRAConnectionID\\": %j{PRAConnectionID},\\"PRAConsoleType\\": %j{PRAConsoleType},\\"PRAApprovalID\\": %d{PRAApprovalID},\\"PRACapabilityPolicyID\\": %d{PRACapabilityPolicyID},\\"PRACredentialPolicyID\\": %d{PRACredentialPolicyID},\\"PRACredentialUserName\\": %j{PRACredentialUserName},\\"PRACredentialLoginType\\": %j{PRACredentialLoginType},\\"PRAErrorStatus\\": %j{PRAErrorStatus},\\"PRAFileTransferList\\": %j{PRAFileTransferList},\\"PRARecordingStatus\\": %j{PRARecordingStatus},\\"PRASharedUsersList\\": %j{PRASharedUsersList},\\"PRASessionType\\": %j{PRASessionType},\\"PRASharedMode\\": %j{PRASharedMode}}\\\\n","name":"tests-magtastamw","description":null,"sessionStatuses":null,"enabled":true,"useTls":true,"policy":{"policyType":"Log Receiver Policy","name":"SIEM selection rule for tests-magtastamw","action":"LOG","status":"enabled"}}', + app_connector_group_ids=[app_connector_group_id], + policy_rules=[ + ("idp", [idp_id]), + ("client_type", ["web_browser", "client_connector"]), + ("scim_group", scim_group_ids), + ], + ) + lss_config_id = lss_config["id"] + except Exception as exc: + errors.append(f"Creating LSS Config failed: {exc}") + + try: + # Test listing LSS Config + all_lss_configs = client.lss.list_configs() + if not any(lss["id"] == lss_config_id for lss in all_lss_configs): + raise AssertionError("LSS Config not found in list") + except Exception as exc: + errors.append(f"Listing LSS Config failed: {exc}") + + # Test retrieving the specific LSS Config + try: + remote_lss = client.lss.get_config(lss_config_id=lss_config_id) + if remote_lss is None: + raise ValueError("Failed to retrieve LSS Config.") + assert remote_lss["id"] == lss_config_id, "Retrieved LSS Config ID does not match" + except Exception as exc: + errors.append(f"Retrieving LSS Config failed: {exc}") + + # Update the LSS Config, particularly changing 'use_tls' to False + try: + updated_description = "Updated " + generate_random_string() + client.lss.update_lss_config( + lss_config_id, description=updated_description, use_tls=False # Explicitly setting use_tls to False + ) + + # Fetch the updated config to verify changes + updated_lss_config = client.lss.get_config(lss_config_id) + if updated_lss_config["config"]["use_tls"] is not False: + raise AssertionError("Failed to update use_tls to False for LSS Config") + + except Exception as exc: + errors.append(f"Updating LSS Config failed: {exc}") + + finally: + # Cleanup resources + if lss_config_id: + try: + client.lss.delete_lss_config(lss_config_id=lss_config_id) + except Exception as exc: + errors.append(f"Deleting LSS Config failed: {exc}") + + if app_connector_group_id: + try: + client.connectors.delete_connector_group(group_id=app_connector_group_id) + except Exception as exc: + errors.append(f"Deleting App Connector Group failed: {exc}") + + assert not errors, f"Errors occurred during the LSS Config lifecycle test: {errors}" diff --git a/tests/integration/zpa/test_lss_log_config.py b/tests/integration/zpa/test_lss_log_config.py new file mode 100644 index 00000000..05c23908 --- /dev/null +++ b/tests/integration/zpa/test_lss_log_config.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import pytest +from tests.integration.zpa.conftest import MockZPAClient + + +@pytest.fixture +def fs(): + yield + + +class TestLSSConfigList: + """ + Integration Tests for the LSS Config Lists + """ + + def test_get_client_types(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + client_types_to_validate = [ + "web_browser", + "machine_tunnel", + "zia_service_edge", + "cloud_connector", + "zia_inspection", + "client_connector", + "zpa_lss", + "client_connector_partner", + "branch_connector", + ] + + try: + # Test without specifying client_type to get all client types + all_client_types = client.lss.get_client_types() + assert len(all_client_types) > 0, "No client types returned" + print("All Client Types:", all_client_types) + + # Test each specific client_type + for client_type in client_types_to_validate: + specific_client_type = client.lss.get_client_types(client_type) + if specific_client_type: + print(f"Specific Client Type for {client_type}:", specific_client_type) + else: + errors.append(f"Client type '{client_type}' not returned correctly") + except Exception as exc: + errors.append(f"Failed to get client types: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + def test_get_log_formats(self, fs): + client = MockZPAClient(fs) + errors = [] # Initialize an empty list to collect errors + log_formats_to_validate = [ + "zpn_sys_auth_log", + "zpn_waf_http_exchanges_log", + "zpn_http_trans_log", + "zpn_ast_comprehensive_stats", + "zpn_auth_log_1id", + "zpn_auth_log", + "zpn_pbroker_comprehensive_stats", + "zpn_ast_auth_log", + "zpn_audit_log", + "zpn_trans_log", + ] + try: + # Test without specifying log_type to get all formats + all_log_formats = client.lss.get_log_formats() + assert len(all_log_formats) > 0, "No log formats returned" + print("All Log Formats:", all_log_formats) + + # Test each specific log_type + for log_type in log_formats_to_validate: + specific_log_format = client.lss.get_log_formats(log_type) + if specific_log_format: + print(f"Specific Log Format for {log_type}:", specific_log_format) + else: + errors.append(f"Log format '{log_type}' not returned correctly or is missing") + except Exception as exc: + errors.append(f"Failed to get log formats: {exc}") + + assert not errors, f"Errors occurred: {errors}" + + # def test_get_status_codes(self, fs): + # client = MockZPAClient(fs) + # errors = [] # Initialize an empty list to collect errors + + # # Define the set of expected status codes for validation + # expected_status_codes = {"zpn_auth_log", "zpn_ast_auth_log", "zpn_trans_log", "zpn_sys_auth_log"} + + # try: + # # Test with default log_type 'all' + # all_status_codes = client.lss.get_status_codes() + # if not all_status_codes: + # errors.append("No status codes returned for default log_type 'all'") + # elif not expected_status_codes.issubset(set(all_status_codes.keys())): + # errors.append("Unexpected status codes returned for default log_type 'all'") + # print("Status Codes for 'all':", all_status_codes) + # except Exception as exc: + # errors.append(f"Failed to get status codes: {exc}") + + # assert not errors, f"Errors occurred during the test: {'; '.join(errors)}" diff --git a/tests/integration/zpa/test_posture_profiles.py b/tests/integration/zpa/test_posture_profiles.py index c8edcd8e..f6bc1cef 100644 --- a/tests/integration/zpa/test_posture_profiles.py +++ b/tests/integration/zpa/test_posture_profiles.py @@ -36,35 +36,48 @@ def test_posture_profile(self, fs): # Attempt to list all posture profiles try: - posture_profile = client.posture_profiles.list_profiles() - assert isinstance(posture_profile, list), "Expected a list of posture profiles" + posture_profiles = client.posture_profiles.list_profiles() + assert isinstance(posture_profiles, list), "Expected a list of posture profiles" + assert len(posture_profiles) > 0, "No posture profiles found" except Exception as exc: errors.append(f"Listing posture profiles failed: {str(exc)}") - # Process each posture profile if the list is not empty - if posture_profile: - for first_profile in posture_profile: - profile_id = first_profile.get("id") - - # Fetch the selected posture profile by its ID - try: - fetched_profile = client.posture_profiles.get_profile(profile_id) - assert fetched_profile is not None, "Expected a valid posture profile object" - assert fetched_profile.get("id") == profile_id, "Mismatch in posture profile ID" - except Exception as exc: - errors.append(f"Fetching posture profile by ID failed: {str(exc)}") - - # Attempt to retrieve the posture profile by name - try: - profile_name = first_profile.get("name") - profile_by_name = client.posture_profiles.get_profile_by_name(profile_name) - assert profile_by_name is not None, "Expected a valid posture profile object when searching by name" - assert profile_by_name.get("id") == profile_id, "Mismatch in posture profile ID when searching by name" - except Exception as exc: - errors.append(f"Fetching posture profile by name failed: {str(exc)}") - - # Once we've tested one profile, exit the loop to avoid redundant testing - break + if posture_profiles: + first_profile = posture_profiles[0] + profile_id = first_profile.get("id") + + # Fetch the selected posture profile by its ID + try: + fetched_profile = client.posture_profiles.get_profile(profile_id) + assert fetched_profile is not None, "Expected a valid posture profile object" + assert fetched_profile.get("id") == profile_id, "Mismatch in posture profile ID" + except Exception as exc: + errors.append(f"Fetching posture profile by ID failed: {str(exc)}") + + # Attempt to retrieve the posture profile by name + try: + profile_name = first_profile.get("name") + profile_by_name = client.posture_profiles.get_profile_by_name(profile_name) + assert profile_by_name is not None, "Expected a valid posture profile object when searching by name" + assert profile_by_name.get("id") == profile_id, "Mismatch in posture profile ID when searching by name" + except Exception as exc: + errors.append(f"Fetching posture profile by name failed: {str(exc)}") + + # Test get_udid_by_profile_name function + try: + profile_udid = client.posture_profiles.get_udid_by_profile_name(profile_name) + assert profile_udid is not None, "Expected a valid UDID when searching by profile name" + assert profile_udid == first_profile.get("posture_udid"), "Mismatch in posture UDID when searching by name" + except Exception as exc: + errors.append(f"Fetching UDID by profile name failed: {str(exc)}") + + # Test get_name_by_posture_udid function + try: + returned_name = client.posture_profiles.get_name_by_posture_udid(profile_udid) + assert returned_name is not None, "Expected a valid profile name when searching by UDID" + assert returned_name == profile_name, "Mismatch in profile name when searching by UDID" + except Exception as exc: + errors.append(f"Fetching name by posture UDID failed: {str(exc)}") # Assert that no errors occurred during the test - assert len(errors) == 0, f"Errors occurred during posture profile operations test: {errors}" + assert len(errors) == 0, f"Errors occurred during posture profile operations test: {'; '.join(errors)}" diff --git a/tests/integration/zpa/test_scim_attributes.py b/tests/integration/zpa/test_scim_attributes.py index c03ab466..9fe6fcfd 100644 --- a/tests/integration/zpa/test_scim_attributes.py +++ b/tests/integration/zpa/test_scim_attributes.py @@ -27,7 +27,7 @@ def fs(): class TestScimAttributes: """ - Integration Tests for the SCIM attributes + Integration Tests for the SCIM attributes. """ def test_scim_attributes_operations(self, fs): @@ -43,21 +43,23 @@ def test_scim_attributes_operations(self, fs): user_idp_id = user_idp["id"] resp = client.scim_attributes.list_attributes_by_idp(user_idp_id) assert isinstance(resp, list), "Response is not in the expected list format." - assert len(resp) > 0, "No SCIM groups were found for the specified IdP." - except Exception as exc: - errors.append(f"Listing SCIM attributes by IDP failed: {exc}") + assert len(resp) > 0, "No SCIM attributes were found for the specified IdP." - try: # Test getting a specific SCIM attribute - attributes = client.scim_attributes.list_attributes_by_idp(user_idp_id) - assert len(attributes) > 0, "No SCIM attributes found for the specified IdP." - - first_attribute_id = attributes[0]["id"] # Assuming attributes is a list of dicts + attributes = resp # Using the previously fetched list of attributes + first_attribute = attributes[0] # Assuming attributes is a list of dicts + first_attribute_id = first_attribute["id"] resp = client.scim_attributes.get_attribute(user_idp_id, first_attribute_id) assert isinstance(resp, dict), "Response is not in the expected dict format." assert resp["id"] == first_attribute_id, "Retrieved SCIM attribute ID does not match the requested ID." + + # Test getting values for the first SCIM attribute + attribute_values = client.scim_attributes.get_values(user_idp_id, first_attribute_id) + assert isinstance(attribute_values, list), "Expected a list of values for the SCIM attribute." + assert len(attribute_values) > 0, "No values returned for the SCIM attribute." + except Exception as exc: - errors.append(f"Getting a specific SCIM attribute failed: {exc}") + errors.append(f"SCIM attribute operation failed: {exc}") # Assert that no errors occurred during the test assert len(errors) == 0, f"Errors occurred during SCIM attributes operations test: {errors}" diff --git a/tests/integration/zpa/test_trusted_networks.py b/tests/integration/zpa/test_trusted_networks.py index 4becbb97..cf9b87ac 100644 --- a/tests/integration/zpa/test_trusted_networks.py +++ b/tests/integration/zpa/test_trusted_networks.py @@ -63,9 +63,20 @@ def test_trusted_networks(self, fs): except Exception as exc: errors.append(f"Failed to fetch network by name: {exc}") + try: + # Test get_network_udid using the network_id of the first network + network_udid = first_network.get("network_id") + network_by_udid = client.trusted_networks.get_network_udid(network_udid) + assert network_by_udid is not None, "Expected a valid trusted network object when searching by network_id" + assert ( + network_by_udid.get("network_id") == network_udid + ), "Mismatch in trusted network network_id when searching by network_id" + except Exception as exc: + errors.append(f"Failed to fetch network by network_id: {exc}") + # Catch any unexpected errors that might not have been caught by inner try-except blocks except Exception as exc: errors.append(f"Unexpected error during trusted networks test: {exc}") # Assert that no errors occurred during the test - assert len(errors) == 0, f"Errors occurred during trusted network operations test: {errors}" + assert not errors, f"Errors occurred during trusted network operations test: {'; '.join(errors)}" diff --git a/zscaler/zpa/__init__.py b/zscaler/zpa/__init__.py index f0fca589..553ff1a3 100644 --- a/zscaler/zpa/__init__.py +++ b/zscaler/zpa/__init__.py @@ -36,7 +36,7 @@ from zscaler.zpa.emergency_access import EmergencyAccessAPI from zscaler.zpa.idp import IDPControllerAPI from zscaler.zpa.inspection import InspectionControllerAPI -from zscaler.zpa.isolation_profile import IsolationProfileAPI +from zscaler.zpa.isolation import IsolationAPI from zscaler.zpa.lss import LSSConfigControllerAPI from zscaler.zpa.machine_groups import MachineGroupsAPI from zscaler.zpa.policies import PolicySetsAPI @@ -187,6 +187,8 @@ def send(self, method, path, json=None, params=None, api_version: str = None): api = self.v2_lss_url elif api_version == "userconfig_v1": api = self.user_config_url + elif api_version == "cbiconfig_v1": + api = self.cbi_url url = f"{api}/{path.lstrip('/')}" start_time = time.time() @@ -456,7 +458,7 @@ def get_paginated_data( params["page"] = page finally: - time.sleep(1) # Ensure a delay between requests regardless of outcome + time.sleep(2) # Ensure a delay between requests regardless of outcome if not ret_data: error_msg = ERROR_MESSAGES["EMPTY_RESULTS"] @@ -498,12 +500,12 @@ def certificates(self): return CertificatesAPI(self) @property - def isolation_profile(self): + def isolation(self): """ - The interface object for the :ref:`ZPA Isolation Profiles `. + The interface object for the :ref:`ZPA Isolation `. """ - return IsolationProfileAPI(self) + return IsolationAPI(self) @property def cloud_connector_groups(self): diff --git a/zscaler/zpa/certificates.py b/zscaler/zpa/certificates.py index bb427149..e76fa3e9 100644 --- a/zscaler/zpa/certificates.py +++ b/zscaler/zpa/certificates.py @@ -171,7 +171,9 @@ def delete_certificate(self, certificate_id: str) -> Box: >>> ba_certificate = zpa.certificates.get_certificate('99999') """ - return self.rest.get(f"certificate/{certificate_id}") + response = self.rest.delete("/certificate/%s" % (certificate_id)) + return response.status_code + # return self.rest.get(f"certificate/{certificate_id}") def get_enrolment(self, certificate_id: str) -> Box: """ diff --git a/zscaler/zpa/inspection.py b/zscaler/zpa/inspection.py index dfdaf98a..cbd09d48 100644 --- a/zscaler/zpa/inspection.py +++ b/zscaler/zpa/inspection.py @@ -45,76 +45,36 @@ def _create_rule(rule: dict) -> dict: ) return rule_set - def add_custom_control( - self, - name: str, - default_action: str, - severity: str, - type: str, - rules: list, - **kwargs, - ) -> Box: + def list_profiles(self, **kwargs) -> BoxList: """ - Adds a new ZPA Inspection Custom Control. + Returns the list of ZPA Inspection Profiles. Args: - name (str): The name of the custom control. - default_action (str): The default action to take for matches against this custom control. - severity (str): The severity for events that match this custom control. - type (str): The type of HTTP message this control matches. - rules (list): A list of Inspection Control rule objects. - **kwargs: Optional keyword args. + **kwargs: + Optional keyword args. Keyword Args: - description (str): Additional information about the custom control. - paranoia_level (int): The paranoia level for the custom control. + **pagesize (int): + Specifies the page size. The default size is 20 and the maximum size is 500. + **search (str, optional): + The search string used to match against features and fields. Returns: - :obj:`Box`: The newly created custom Inspection Control resource record. - - """ - - payload = { - "name": name, - "defaultAction": default_action, - "severity": severity, - "rules": [], - "type": type, - } - - # Handle default_action_value - if "default_action_value" in kwargs: - payload["defaultActionValue"] = kwargs["default_action_value"] - - # Use the _create_rule method to restructure the Inspection Control rule and add to the payload. - for rule in rules: - payload["rules"].append(self._create_rule(rule)) + :obj:`BoxList`: The list of ZPA Inspection Profile resource records. - # Add optional parameters to payload - for key, value in kwargs.items(): - if key == "paranoia_level": - payload["paranoiaLevel"] = int(value) - elif key == "description": - payload["description"] = value - # Add other optional parameters if necessary + Examples: + Iterate over all ZPA Inspection Profiles and print them: - # Convert snake to camelcase - payload = convert_keys(payload) + .. code-block:: python - response = self.rest.post("inspectionControls/custom", json=payload) - if isinstance(response, Response): - # this is only true when the creation failed (status code is not 2xx) - status_code = response.status_code - # Handle error response - raise Exception(f"API call failed with status {status_code}: {response.json()}") - return response + for profile in zpa.inspection.list_profiles(): + print(profile) - # response = self.rest.post("/inspectionControls/custom", json=payload) - # if isinstance(response, Response): - # status_code = response.status_code - # if status_code > 299: - # return None - # return self.get_custom_control(response.get("id")) + """ + list, _ = self.rest.get_paginated_data( + path="/inspectionProfile", + ) + return list def add_profile(self, name: str, paranoia_level: int, predef_controls_version: str, **kwargs): """ @@ -233,33 +193,107 @@ def add_profile(self, name: str, paranoia_level: int, predef_controls_version: s raise Exception(f"API call failed with status {status_code}: {response.json()}") return response - # response = self.rest.post("/inspectionProfile", json=payload) - # if isinstance(response, Response): - # status_code = response.status_code - # if status_code > 299: - # return None - # return self.get_profile(response.get("id")) - - def delete_custom_control(self, control_id: str) -> int: + def update_profile(self, profile_id: str, **kwargs): """ - Deletes the specified custom ZPA Inspection Control. + Updates the specified ZPA Inspection Profile. Args: - control_id (str): - The unique id for the custom control that will be deleted. + profile_id (str): + The unique id for the ZPA Inspection Profile that will be updated. + predef_controls_version (str): + The predefined controls version for the ZPA Inspection Profile. Defaults to `OWASP_CRS/3.3.0`. + **kwargs: + Optional keyword args. + + Keyword Args: + **custom_controls (list): + A tuple list of custom controls to be added to the Inspection profile. + + Custom control tuples must follow the convention below: + + ``(control_id, action)`` + + e.g. + + .. code-block:: python + + custom_controls = [(99999, "BLOCK"), (88888, "PASS")] + **description (str): + Additional information about the Inspection Profile. + **name (str): + The name of the Inspection Profile. + **paranoia_level (int): + The paranoia level for the Inspection Profile. + **predef_controls (list): + A tuple list of predefined controls to be added to the Inspection profile. + + Predefined control tuples must follow the convention below: + + ``(control_id, action)`` + + e.g. + + .. code-block:: python + + predef_controls = [(77777, "BLOCK"), (66666, "PASS")] + **predef_controls_version (str): + The version of the predefined controls that will be added. Returns: - :obj:`int`: The status code for the operation. + :obj:`Box`: The updated ZPA Inspection Profile resource record. Examples: - Delete a custom ZPA Inspection Control with an id of `99999`. + Update the name and description of a ZPA Inspection Profile with the id 99999: .. code-block:: python - zpa.inspection.delete_custom_control("99999") + print( + zpa.inspection.update_profile( + "99999", + name="inspect_common_predef_controls", + description="Inspects common controls from the Predefined set.", + ) + ) + + Add a custom control to the ZPA Inspection Profile with the id 88888: + + .. code-block:: python + + print( + zpa.inspection.update_profile( + "88888", + custom_controls=[("2", "BLOCK")], + ) + ) """ - return self.rest.delete(f"inspectionControls/custom/{control_id}").status_code + # Set payload to value of existing record + payload = self.get_profile(profile_id) + payload["predefinedControlsVersion"] = kwargs.get("predef_controls_version", "OWASP_CRS/3.3.0") + + # Extend existing list of default predefined controls if the user supplies more + if kwargs.get("predef_controls"): + controls = kwargs.pop("predef_controls") + for control in controls: + payload["predefined_controls"] = [{"id": control[0], "action": control[1]} for control in controls] + + # Add custom controls if provided + if kwargs.get("custom_controls"): + controls = kwargs.pop("custom_controls") + payload["custom_controls"] = [{"id": control[0], "action": control[1]} for control in controls] + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload[key] = value + + # Convert from snake case to camel case + payload = convert_keys(payload) + + resp = self.rest.put(f"inspectionProfile/{profile_id}", json=payload).status_code + + # Return the object if it was updated successfully + if not isinstance(resp, Response): + return self.get_profile(profile_id) def delete_profile(self, profile_id: str): """ @@ -282,32 +316,6 @@ def delete_profile(self, profile_id: str): """ return self.rest.delete(f"inspectionProfile/{profile_id}").status_code - def get_custom_control(self, control_id: str) -> Box: - """ - Returns the specified custom ZPA Inspection Control. - - Args: - control_id (str): - The unique id of the custom ZPA Inspection Control to be returned. - - Returns: - :obj:`Box`: The custom ZPA Inspection Control resource record. - - Examples: - Print the Custom Inspection Control with an id of `99999`: - - .. code-block:: python - - print(zpa.inspection.get_custom_control("99999")) - - """ - response = self.rest.get("/inspectionControls/custom/%s" % (control_id)) - if isinstance(response, Response): - status_code = response.status_code - if status_code != 200: - return None - return response - def get_predef_control(self, control_id: str): """ Returns the specified predefined ZPA Inspection Control. @@ -360,113 +368,281 @@ def get_profile(self, profile_id: str) -> Box: return None return response - def list_control_action_types(self) -> Box: + def list_custom_controls(self, **kwargs) -> BoxList: """ - Returns a list of ZPA Inspection Control Action Types. - - Returns: - :obj:`BoxList`: A list containing the ZPA Inspection Control Action Types. - - Examples: - Iterate over the ZPA Inspection Control Action Types and print each one: + Returns a list of all custom ZPA Inspection Controls. - .. code-block:: python + Args: + **kwargs: Optional keyword arguments. - for action_type in zpa.inspection.list_control_action_types(): - print(action_type) + Keyword Args: + **search (str): + The string used to search for a custom control by features and fields. + **sortdir (str): + Specifies the sorting order for the search results. - """ - return self.rest.get("inspectionControls/actionTypes") + Accepted values are: - def list_control_severity_types(self) -> BoxList: - """ - Returns a list of Inspection Control Severity Types. + - ``ASC`` - ascending order + - ``DESC`` - descending order Returns: - :obj:`BoxList`: A list containing all valid Inspection Control Severity Types. + :obj:`BoxList`: A list containing all custom ZPA Inspection Controls. Examples: - Print all Inspection Control Severity Types + Print a list of all custom ZPA Inspection Controls: .. code-block:: python - for severity in zpa.inspection.list_control_severity_types(): - print(severity) + for control in zpa.inspection.list_custom_controls(): + print(control) """ - return self.rest.get("inspectionControls/severityTypes") + list, _ = self.rest.get_paginated_data( + path="/inspectionControls/custom", + ) + return list - def list_control_types(self) -> BoxList: + def get_custom_control(self, control_id: str) -> Box: """ - Returns a list of ZPA Inspection Control Types. + Returns the specified custom ZPA Inspection Control. + + Args: + control_id (str): + The unique id of the custom ZPA Inspection Control to be returned. Returns: - :obj:`BoxList`: A list containing ZPA Inspection Control Types. + :obj:`Box`: The custom ZPA Inspection Control resource record. Examples: - Print all ZPA Inspection Control Types: + Print the Custom Inspection Control with an id of `99999`: .. code-block:: python - for control_type in zpa.inspection.list_control_types(): - print(control_type) - - """ - return self.rest.get("inspectionControls/controlTypes") + print(zpa.inspection.get_custom_control("99999")) - def list_custom_control_types(self) -> BoxList: """ - Returns a list of custom ZPA Inspection Control Types. - + response = self.rest.get("/inspectionControls/custom/%s" % (control_id)) + if isinstance(response, Response): + status_code = response.status_code + if status_code != 200: + return None + return response + + def add_custom_control( + self, + name: str, + default_action: str, + severity: str, + type: str, + rules: list, + **kwargs, + ) -> Box: + """ + Adds a new ZPA Inspection Custom Control. + + Args: + name (str): The name of the custom control. + default_action (str): The default action to take for matches against this custom control. + severity (str): The severity for events that match this custom control. + type (str): The type of HTTP message this control matches. + rules (list): A list of Inspection Control rule objects. + **kwargs: Optional keyword args. + + Keyword Args: + description (str): Additional information about the custom control. + paranoia_level (int): The paranoia level for the custom control. + Returns: - :obj:`BoxList`: A list containing custom ZPA Inspection Control Types. + :obj:`Box`: The newly created custom Inspection Control resource record. Examples: - - Print all custom ZPA Inspection Control Types + Create a new custom Inspection Control with the minimum required parameters .. code-block:: python - for control_type in zpa.inspection.list_custom_control_types(): - print(control_type) - + print( + zpa.inspection.add_custom_control( + "test8", + severity="INFO", + description="test descr", + paranoia_level="3", + type="REQUEST", + default_action="BLOCK", + rules=[ + { + "names": ["test"], + "type": "REQUEST_HEADERS", + "conditions": [("SIZE", "GE", "10"), ("VALUE", "CONTAINS", "test")], + } + ], + ) + ) """ - return self.rest.get("https://config.private.zscaler.com/mgmtconfig/v1/admin/inspectionControls/customControlTypes") - def list_custom_controls(self, **kwargs) -> BoxList: + payload = { + "name": name, + "defaultAction": default_action, + "severity": severity, + "rules": [], + "type": type, + } + + # Handle default_action_value + if "default_action_value" in kwargs: + payload["defaultActionValue"] = kwargs["default_action_value"] + + # Use the _create_rule method to restructure the Inspection Control rule and add to the payload. + for rule in rules: + payload["rules"].append(self._create_rule(rule)) + + # Add optional parameters to payload + for key, value in kwargs.items(): + if key == "paranoia_level": + payload["paranoiaLevel"] = int(value) + elif key == "description": + payload["description"] = value + # Add other optional parameters if necessary + + # Convert snake to camelcase + payload = convert_keys(payload) + + response = self.rest.post("inspectionControls/custom", json=payload) + if isinstance(response, Response): + # this is only true when the creation failed (status code is not 2xx) + status_code = response.status_code + # Handle error response + raise Exception(f"API call failed with status {status_code}: {response.json()}") + return response + + def update_custom_control(self, control_id: str, **kwargs) -> Box: """ - Returns a list of all custom ZPA Inspection Controls. + Updates the specified custom ZPA Inspection Control. Args: - **kwargs: Optional keyword arguments. + control_id (str): + The unique id for the custom control that will be updated. + **kwargs: + Optional keyword args. Keyword Args: - **search (str): - The string used to search for a custom control by features and fields. - **sortdir (str): - Specifies the sorting order for the search results. + **description (str): + Additional information about the custom control. + **default_action (str): + The default action to take for matches against this custom control. Valid options are: - Accepted values are: + - ``PASS`` + - ``BLOCK`` + - ``REDIRECT`` + **name (str): + The name of the custom control. + **paranoia_level (int): + The paranoia level for the custom control. + **rules (list): + A list of Inspection Control rule objects, with each object using the format:: - - ``ASC`` - ascending order - - ``DESC`` - descending order + { + "names": ["name1", "name2"], + "type": "rule_type", + "conditions": [ + ("LHS", "OP", "RHS"), + ("LHS", "OP", "RHS"), + ], + } + **severity (str): + The severity for events that match this custom control. Valid options are: + + - ``CRITICAL`` + - ``ERROR`` + - ``WARNING`` + - ``INFO`` + **type (str): + The type of HTTP message this control matches. Valid options are: + + - ``REQUEST`` + - ``RESPONSE`` Returns: - :obj:`BoxList`: A list containing all custom ZPA Inspection Controls. + :obj:`Box`: The updated custom ZPA Inspection Control resource record. Examples: - Print a list of all custom ZPA Inspection Controls: + Update the description of a custom ZPA Inspection Control with an id of 99999: .. code-block:: python - for control in zpa.inspection.list_custom_controls(): - print(control) + print( + zpa.inspection.update_custom_control( + "99999", + description="Updated description", + ) + ) + + Update the rules of a custom ZPA Inspection Control with an id of 88888: + + .. code-block:: python + print( + zpa.inspection.update_custom_control( + "88888", + rules=[ + { + "names": ["xforwardedfor_ge_20"], + "type": "REQUEST_HEADERS", + "conditions": [ + ("SIZE", "GE", "20"), + ("VALUE", "CONTAINS", "X-Forwarded-For"), + ], + } + ], + ) + ) """ - list, _ = self.rest.get_paginated_data( - path="/inspectionControls/custom", - ) - return list + # Set payload to value of existing record and recursively convert nested dict keys from snake_case + # to camelCase. + payload = convert_keys(self.get_custom_control(control_id)) + + # If the user provides rules for an update, clear the current rules then use the create_rule method to + # restructure the Inspection Control rule and add to the payload. + if kwargs.get("rules"): + payload["rules"] = [] + for rule in kwargs.pop("rules"): + payload["rules"].append(self._create_rule(rule)) + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload_key = snake_to_camel(key) + payload[payload_key] = value + + # Special handling for default_action_value + if key == "default_action_value": + payload["defaultActionValue"] = value + + resp = self.rest.put(f"inspectionControls/custom/{control_id}", json=payload).status_code + + # Return the object if it was updated successfully + if not isinstance(resp, Response): + return self.get_custom_control(control_id) + + def delete_custom_control(self, control_id: str) -> int: + """ + Deletes the specified custom ZPA Inspection Control. + + Args: + control_id (str): + The unique id for the custom control that will be deleted. + + Returns: + :obj:`int`: The status code for the operation. + + Examples: + Delete a custom ZPA Inspection Control with an id of `99999`. + + .. code-block:: python + + zpa.inspection.delete_custom_control("99999") + + """ + return self.rest.delete(f"inspectionControls/custom/{control_id}").status_code def list_custom_http_methods(self) -> BoxList: """ @@ -622,37 +798,6 @@ def get_predef_control_group_by_name(self, group_name: str, version: str = "OWAS # If we reach here, the control group was not found raise ValueError(f"No predefined control group named '{group_name}' found") - def list_profiles(self, **kwargs) -> BoxList: - """ - Returns the list of ZPA Inspection Profiles. - - Args: - **kwargs: - Optional keyword args. - - Keyword Args: - **pagesize (int): - Specifies the page size. The default size is 20 and the maximum size is 500. - **search (str, optional): - The search string used to match against features and fields. - - Returns: - :obj:`BoxList`: The list of ZPA Inspection Profile resource records. - - Examples: - Iterate over all ZPA Inspection Profiles and print them: - - .. code-block:: python - - for profile in zpa.inspection.list_profiles(): - print(profile) - - """ - list, _ = self.rest.get_paginated_data( - path="/inspectionProfile", - ) - return list - def profile_control_attach(self, profile_id: str, action: str, **kwargs) -> Box: """ Attaches or detaches all predefined ZPA Inspection Controls to a ZPA Inspection Profile. @@ -719,250 +864,91 @@ def profile_control_attach(self, profile_id: str, action: str, **kwargs) -> Box: else: raise ValueError("Unknown action provided. Valid actions are 'attach' or 'detach'.") - def update_custom_control(self, control_id: str, **kwargs) -> Box: + def update_profile_and_controls(self, profile_id: str, inspection_profile: dict, **kwargs): """ - Updates the specified custom ZPA Inspection Control. - - Args: - control_id (str): - The unique id for the custom control that will be updated. - **kwargs: - Optional keyword args. - - Keyword Args: - **description (str): - Additional information about the custom control. - **default_action (str): - The default action to take for matches against this custom control. Valid options are: - - - ``PASS`` - - ``BLOCK`` - - ``REDIRECT`` - **name (str): - The name of the custom control. - **paranoia_level (int): - The paranoia level for the custom control. - **rules (list): - A list of Inspection Control rule objects, with each object using the format:: - - { - "names": ["name1", "name2"], - "type": "rule_type", - "conditions": [ - ("LHS", "OP", "RHS"), - ("LHS", "OP", "RHS"), - ], - } - **severity (str): - The severity for events that match this custom control. Valid options are: - - - ``CRITICAL`` - - ``ERROR`` - - ``WARNING`` - - ``INFO`` - **type (str): - The type of HTTP message this control matches. Valid options are: - - - ``REQUEST`` - - ``RESPONSE`` - - Returns: - :obj:`Box`: The updated custom ZPA Inspection Control resource record. - - Examples: - Update the description of a custom ZPA Inspection Control with an id of 99999: - - .. code-block:: python - - print( - zpa.inspection.update_custom_control( - "99999", - description="Updated description", - ) - ) - - Update the rules of a custom ZPA Inspection Control with an id of 88888: + Updates the inspection profile and controls for the specified ID. - .. code-block:: python + Note: + This method has not been fully implemented and will not be maintained. There seems to be functionality + duplication with the default Inspection Profile update API call. `**kwargs` has been provided as a parameter + for you to be able to add any additional args that Zscaler may add. - print( - zpa.inspection.update_custom_control( - "88888", - rules=[ - { - "names": ["xforwardedfor_ge_20"], - "type": "REQUEST_HEADERS", - "conditions": [ - ("SIZE", "GE", "20"), - ("VALUE", "CONTAINS", "X-Forwarded-For"), - ], - } - ], - ) - ) + If you feel that this is in error and that this functionality should be correctly implemented by zscaler-sdk-python + `raise an issue ` in the zscaler-sdk-python Github repo + Args: + profile_id (str): + The unique id of the inspection profile. + inspection_profile (dict): + The new inspection profile object. + **kwargs: + Additional keyword args. """ - # Set payload to value of existing record and recursively convert nested dict keys from snake_case - # to camelCase. - payload = convert_keys(self.get_custom_control(control_id)) - - # If the user provides rules for an update, clear the current rules then use the create_rule method to - # restructure the Inspection Control rule and add to the payload. - if kwargs.get("rules"): - payload["rules"] = [] - for rule in kwargs.pop("rules"): - payload["rules"].append(self._create_rule(rule)) - - # Add optional parameters to payload - for key, value in kwargs.items(): - payload_key = snake_to_camel(key) - payload[payload_key] = value + payload = { + "inspection_profile_id": profile_id, + "inspection_profile": inspection_profile, + } - # Special handling for default_action_value - if key == "default_action_value": - payload["defaultActionValue"] = value + payload = convert_keys(payload) - resp = self.rest.put(f"inspectionControls/custom/{control_id}", json=payload).status_code + resp = self.rest.put("inspectionProfile/{profile_id}/patch", json=payload).status_code # Return the object if it was updated successfully if not isinstance(resp, Response): - return self.get_custom_control(control_id) + return self.get_profile(profile_id) - def update_profile(self, profile_id: str, **kwargs): + def list_control_action_types(self) -> Box: """ - Updates the specified ZPA Inspection Profile. - - Args: - profile_id (str): - The unique id for the ZPA Inspection Profile that will be updated. - predef_controls_version (str): - The predefined controls version for the ZPA Inspection Profile. Defaults to `OWASP_CRS/3.3.0`. - **kwargs: - Optional keyword args. - - Keyword Args: - **custom_controls (list): - A tuple list of custom controls to be added to the Inspection profile. - - Custom control tuples must follow the convention below: - - ``(control_id, action)`` - - e.g. - - .. code-block:: python - - custom_controls = [(99999, "BLOCK"), (88888, "PASS")] - **description (str): - Additional information about the Inspection Profile. - **name (str): - The name of the Inspection Profile. - **paranoia_level (int): - The paranoia level for the Inspection Profile. - **predef_controls (list): - A tuple list of predefined controls to be added to the Inspection profile. - - Predefined control tuples must follow the convention below: - - ``(control_id, action)`` - - e.g. - - .. code-block:: python - - predef_controls = [(77777, "BLOCK"), (66666, "PASS")] - **predef_controls_version (str): - The version of the predefined controls that will be added. + Returns a list of ZPA Inspection Control Action Types. Returns: - :obj:`Box`: The updated ZPA Inspection Profile resource record. + :obj:`BoxList`: A list containing the ZPA Inspection Control Action Types. Examples: - Update the name and description of a ZPA Inspection Profile with the id 99999: - - .. code-block:: python - - print( - zpa.inspection.update_profile( - "99999", - name="inspect_common_predef_controls", - description="Inspects common controls from the Predefined set.", - ) - ) - - Add a custom control to the ZPA Inspection Profile with the id 88888: + Iterate over the ZPA Inspection Control Action Types and print each one: .. code-block:: python - print( - zpa.inspection.update_profile( - "88888", - custom_controls=[("2", "BLOCK")], - ) - ) + for action_type in zpa.inspection.list_control_action_types(): + print(action_type) """ - # Set payload to value of existing record - payload = self.get_profile(profile_id) - payload["predefinedControlsVersion"] = kwargs.get("predef_controls_version", "OWASP_CRS/3.3.0") - - # Extend existing list of default predefined controls if the user supplies more - if kwargs.get("predef_controls"): - controls = kwargs.pop("predef_controls") - for control in controls: - payload["predefined_controls"] = [{"id": control[0], "action": control[1]} for control in controls] + return self.rest.get("inspectionControls/actionTypes") - # Add custom controls if provided - if kwargs.get("custom_controls"): - controls = kwargs.pop("custom_controls") - payload["custom_controls"] = [{"id": control[0], "action": control[1]} for control in controls] + def list_control_severity_types(self) -> BoxList: + """ + Returns a list of Inspection Control Severity Types. - # Add optional parameters to payload - for key, value in kwargs.items(): - payload[key] = value + Returns: + :obj:`BoxList`: A list containing all valid Inspection Control Severity Types. - # Convert from snake case to camel case - payload = convert_keys(payload) + Examples: + Print all Inspection Control Severity Types - resp = self.rest.put(f"inspectionProfile/{profile_id}", json=payload).status_code + .. code-block:: python - # Return the object if it was updated successfully - if not isinstance(resp, Response): - return self.get_profile(profile_id) + for severity in zpa.inspection.list_control_severity_types(): + print(severity) - def update_profile_and_controls(self, profile_id: str, inspection_profile: dict, **kwargs): """ - Updates the inspection profile and controls for the specified ID. - - Note: - This method has not been fully implemented and will not be maintained. There seems to be functionality - duplication with the default Inspection Profile update API call. `**kwargs` has been provided as a parameter - for you to be able to add any additional args that Zscaler may add. - - If you feel that this is in error and that this functionality should be correctly implemented by zscaler-sdk-python - `raise an issue ` in the zscaler-sdk-python Github repo - - Args: - profile_id (str): - The unique id of the inspection profile. - inspection_profile (dict): - The new inspection profile object. - **kwargs: - Additional keyword args. + return self.rest.get("inspectionControls/severityTypes") + def list_control_types(self) -> BoxList: """ + Returns a list of ZPA Inspection Control Types. - payload = { - "inspection_profile_id": profile_id, - "inspection_profile": inspection_profile, - } + Returns: + :obj:`BoxList`: A list containing ZPA Inspection Control Types. - payload = convert_keys(payload) + Examples: + Print all ZPA Inspection Control Types: - resp = self.rest.put("inspectionProfile/{profile_id}/patch", json=payload).status_code + .. code-block:: python - # Return the object if it was updated successfully - if not isinstance(resp, Response): - return self.get_profile(profile_id) + for control_type in zpa.inspection.list_control_types(): + print(control_type) + + """ + return self.rest.get("inspectionControls/controlTypes") diff --git a/zscaler/zpa/isolation.py b/zscaler/zpa/isolation.py new file mode 100644 index 00000000..5d375a61 --- /dev/null +++ b/zscaler/zpa/isolation.py @@ -0,0 +1,681 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2023, Zscaler Inc. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +from box import Box, BoxList +from requests import Response + +from zscaler.utils import snake_to_camel, recursive_snake_to_camel + +from zscaler.zpa.client import ZPAClient + + +class IsolationAPI: + def __init__(self, client: ZPAClient): + self.rest = client + + def list_banners(self: str) -> Box: + """ + Returns information a list of all cloud browser isolation banners. + + Args: + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation. + + Examples: + >>> pprint(zpa.isolation.list_banners()) + + """ + return self.rest.get("banners", api_version="cbiconfig_v1") + + def get_banner(self, banner_id: str) -> Box: + """ + Returns information on the specified cloud browser isolation banner. + + Args: + profile_id (str): + The unique identifier for the cloud browser isolation banner. + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation banner. + + Examples: + >>> pprint(zpa.isolation.get_banner('99999')) + + """ + return self.rest.get(f"banners/{banner_id}", api_version="cbiconfig_v1") + + def add_banner(self, name: str, banner: bool, **kwargs) -> Box: + """ + Adds a new cloud browser isolation banner. + + Args: + name (str): + The name of the new cloud browser isolation banner. + logo (str): + Base64 Logo Image (.jpeg or .png; Maximum file size is 100KB.) + + **kwargs: + + Keyword Args: + primary_color (str): + Cloud browser isolation Banner Primary Color + text_color (str): + Cloud browser isolation Banner Text Color + banner (bool): + Enable Cloud browser isolation banner + notification_title (str): + Cloud browser isolation Banner Notification Title + notification_text (str): + Cloud browser isolation Banner Notification Text + + Returns: + :obj:`Box`: The resource record for the newly created Cloud browser isolation. + + Examples: + Creating a Cloud browser isolation with the minimum required parameters: + + >>> zpa.isolation.add_banner( + ... name='new_banner', + ... logo= 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYQAAABQCAMAAAAuu', + ... primary_color='#0076BE', + ... text_color='#FFFFFF', + ... banner=True, + ... notification_title='Heads up, you have been redirected to Browser Isolation!', + ... notification_text='ZscalerCloud Browser Isolation', + ... ) + """ + + payload = { + "name": name, + "banner": banner, + } + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload[snake_to_camel(key)] = value + + response = self.rest.post("banner", json=payload, api_version="cbiconfig_v1") + if isinstance(response, Response): + # this is only true when the creation failed (status code is not 2xx) + status_code = response.status_code + # Handle error response + raise Exception(f"API call failed with status {status_code}: {response.json()}") + return response + + def update_banner(self, banner_id: str, **kwargs) -> Box: + """ + Updates an existing Cloud browser isolation. + + Args: + banner_id (str): + The unique identifier for the Cloud browser isolation to be updated. + **kwargs: Optional keyword args. + + Keyword Args: + name (str): + Cloud browser isolation Theme Name + primary_color (str): + Cloud browser isolation Banner Primary Color + text_color (str): + Cloud browser isolation Banner Text Color + banner (bool): + Enable Cloud browser isolation banner + notification_title (str): + Cloud browser isolation Banner Notification Title + notification_text (str): + Cloud browser isolation Banner Notification Text + + Returns: + :obj:`Box`: The resource record for the updated Cloud browser isolation. + + Examples: + Updating the name of a Cloud browser isolation: + + >>> zpa.isolation.update_banner( + banner_id='99999', + ... name='updated_name') + + """ + # Set payload to value of existing record + payload = {snake_to_camel(k): v for k, v in self.get_banner(banner_id).items()} + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload[snake_to_camel(key)] = value + + resp = self.rest.put(f"banners/{banner_id}", json=payload, api_version="cbiconfig_v1") + # Return the object if it was updated successfully + if not isinstance(resp, Response): + return self.get_banner(banner_id) + + def delete_banner(self, banner_id: str) -> int: + """ + Deletes the specified Cloud browser isolation. + + Args: + banner_id (str): + The unique identifier for the Cloud browser isolation to be deleted. + + Returns: + :obj:`int`: The response code for the operation. + + Examples: + >>> zpa.isolation.delete_banner('99999') + + """ + return self.rest.delete(f"banners/{banner_id}", api_version="cbiconfig_v1").status_code + + def list_certificates(self: str) -> Box: + """ + Returns information on the specified Cloud browser isolation. + + Args: + + Returns: + :obj:`Box`: The resource record for the Cloud browser isolation. + + Examples: + >>> pprint(zpa.isolation.list_certificates()) + + """ + return self.rest.get("certificates", api_version="cbiconfig_v1") + + def get_certificate(self, certificate_id: str) -> Box: + """ + Returns information on the specified cloud browser certificate. + + Args: + certificate_id (str): + The unique identifier for the cloud browser certificate ID. + + Returns: + :obj:`Box`: The resource record for the cloud browser certificate. + + Examples: + >>> pprint(zpa.isolation.get_certificate('99999')) + + """ + return self.rest.get(f"certificates/{certificate_id}", api_version="cbiconfig_v1") + + def add_certificate(self, name, pem: str, **kwargs) -> Box: + """ + Adds a new Cloud browser isolation. + + Args: + name (str): + The name of the new Cloud browser isolation. + pem (str): + The content of the certificate in PEM format. + + Returns: + :obj:`Box`: The resource record for the newly created Cloud browser isolation. + + Examples: + Creating a Cloud browser isolation with the minimum required parameters: + + >>> zpa.isolation.add_certificate( + ... name='new_certificate', + ... pem=("-----BEGIN CERTIFICATE-----\\n" + ... "nMIIF2DCCA8CgAwIBAgIBATANBgkqhkiG==\\n" + ... "-----END CERTIFICATE-----"), + ) + + """ + + payload = { + "name": name, + "pem": pem, + } + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload[snake_to_camel(key)] = value + + response = self.rest.post("certificate", json=payload, api_version="cbiconfig_v1") + if isinstance(response, Response): + # this is only true when the creation failed (status code is not 2xx) + status_code = response.status_code + # Handle error response + raise Exception(f"API call failed with status {status_code}: {response.json()}") + return response + + def update_certificate(self, certificate_id: str, **kwargs) -> Box: + """ + Updates an existing cloud browser isolation certificate. + + Args: + certificate_id (str): + The unique identifier for the cloud browser isolation certificate to be updated. + name (str): + The name of the new cloud browser isolation certificate. + pem (str): + The content of the certificate in PEM format. + + Returns: + :obj:`Box`: The resource record for the updated Cloud browser isolation. + + Examples: + Updating the name of a Cloud browser isolation: + + >>> zpa.isolation.update_certificate( + ... name='new_certificate', + ... pem=("-----BEGIN CERTIFICATE-----\\n" + ... "MIIFNzCCBIHNIHIO==\\n" + ... "-----END CERTIFICATE-----"), + ) + """ + # Set payload to value of existing record + payload = {snake_to_camel(k): v for k, v in self.get_certificate(certificate_id).items()} + + # Add optional parameters to payload + for key, value in kwargs.items(): + payload[snake_to_camel(key)] = value + + resp = self.rest.put(f"certificates/{certificate_id}", json=payload, api_version="cbiconfig_v1") + # Return the object if it was updated successfully + if not isinstance(resp, Response): + return self.get_certificate(certificate_id) + + def delete_certificate(self, certificate_id: str) -> int: + """ + Deletes the specified cloud browser isolation certificate. + + Args: + certificate_id (str): + The unique identifier for the Cloud browser isolation to be deleted. + + Returns: + :obj:`int`: The response code for the operation. + + Examples: + >>> zpa.isolation.delete_certificate('99999') + + """ + return self.rest.delete(f"certificates/{certificate_id}", api_version="cbiconfig_v1").status_code + + def list_profiles(self, **kwargs) -> BoxList: + """ + Returns a list of all configured isolation profiles. + + Keyword Args: + max_items (int): The maximum number of items to request before stopping iteration. + max_pages (int): The maximum number of pages to request before stopping iteration. + pagesize (int): Specifies the page size. The default size is 20, but the maximum size is 500. + search (str, optional): The search string used to match against features and fields. + + Returns: + BoxList: A list of all configured isolation profiles. + + Examples: + >>> for isolation_profile in zpa.isolation_profiles.list_profiles(): + ... pprint(isolation_profile) + """ + list, _ = self.rest.get_paginated_data(path="/isolation/profiles", **kwargs) + return list + + def get_profile_by_name(self, name: str): + """ + Retrieves a specific isolation profile by its name. + + Args: + name (str): The name of the isolation profile to search for. + + Returns: + dict or None: The isolation profile with the specified name if found, otherwise None. + + Examples: + >>> profile = zpa.isolation_profiles.get_profile_by_name('DefaultProfile') + >>> print(profile) + """ + profiles = self.list_profiles() + for profile in profiles: + if profile.get("name") == name: + return profile + return None + + def get_profile_by_id(self, profile_id: str): + """ + Retrieves a specific isolation profile by its unique identifier (ID). + + Args: + profile_id (str): The ID of the isolation profile to retrieve. + + Returns: + dict or None: The isolation profile with the specified ID if found, otherwise None. + + Examples: + >>> profile = zpa.isolation_profiles.get_profile_by_id('12345') + >>> print(profile) + """ + profiles = self.list_profiles() + for profile in profiles: + if str(profile.get("id")) == str(profile_id): # Ensuring ID comparison as strings + return profile + return None + + def list_cbi_profiles(self: str) -> Box: + """ + Returns information on the specified Cloud browser isolation. + + Args: + + Returns: + :obj:`Box`: The resource record for the Cloud browser isolation. + + Examples: + >>> pprint(zpa.isolation.list_cbi_profiles()) + + """ + return self.rest.get("profiles", api_version="cbiconfig_v1") + + def get_cbi_profile(self, profile_id: str) -> Box: + """ + Returns information on the specified cloud browser isolation certificate. + + Args: + profile_id (str): + The unique identifier for the cloud browser isolation certificate ID. + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation certificate. + + Examples: + >>> pprint(zpa.isolation.get_cbi_profile('99999')) + + """ + return self.rest.get(f"profiles/{profile_id}", api_version="cbiconfig_v1") + + def add_cbi_profile(self, name: str, region_ids, certificate_ids: list, **kwargs) -> Box: + """ + Adds a new cloud browser isolation profile to the Zscaler platform. + + Args: + name (str): The name of the new cloud browser isolation profile. + region_ids (list): List of region IDs. Requires at least 2 region IDs. + certificate_ids (list): List of certificate IDs associated with the profile. + + Keyword Args: + description (str, optional): A brief description of the security profile. + is_default (bool, optional): Indicates if this profile should be set as the default for new users. + banner_id (str, optional): The unique identifier for a custom banner displayed in the isolation session. + security_controls (dict, optional): Specifies the cloud browser isolation security settings. + + - document_viewer (bool): Enable or disable document viewing capabilities + - allow_printing (bool): Allow or restrict printing of documents + - watermark (dict): Configuration for watermarking documents displayed in the browser: + - enabled (bool): Enable or disable watermarking + - show_user_id (bool): Display user ID on the watermark. + - show_timestamp (bool): Include a timestamp in the watermark. + - show_message (bool): Include a custom message in the watermark. + - message (str): The custom message to display if 'show_message' is True. + + - flattened_pdf (bool): Specify whether PDFs should be flattened. + - upload_download (str): Control upload and download capabilities ('all', 'none', or other configurations). + - restrict_keystrokes (bool): Restrict the use of keystrokes within the isolation session. + - copy_paste (str): Control copy and paste capabilities ('all', 'none', or specific configurations). + - local_render (bool): Enable or disable local rendering of web content. + + debug_mode (dict, optional): Debug mode settings that may include logging and error tracking configurations. + + - allowed (bool, optional): Allow debug mode + - file_password (str, Optional): Optional password to debug files when this mode is enabled. + + user_experience (dict, optional): Settings that affect how end-users interact with the isolated browser. + + - forward_to_zia (dict): Configuration for forwarding traffic to ZIA: + - enabled (bool): Enable or disable forwarding. + - organization_id (str): Organization ID to use for forwarding. + - cloud_name (str): Name of the Zscaler cloud. + - pac_file_url (str): URL to the PAC file. + - browser_in_browser (bool): Enable or disable the use of a browser within the isolated browser. + - persist_isolation_bar (bool): Specify whether the isolation bar should remain visible. + - session_persistence (bool): Enable or disable session persistence across browser restarts. + + Returns: + :obj:`Box`: The resource record for the newly created Cloud browser isolation profile. + + Examples: + Creating a security profile with required and optional parameters: + + >>> zpa.isolation.add_cbi_profile( + ... name='Add_CBI_Profile', + ... region_ids=["dc75dc8d-a713-49aa-821e-eb35da523cc2", "1a2cd1bc-b8e0-466b-96ad-fbe44832e1c7"], + ... certificate_ids=["87122222-457f-11ed-b878-0242ac120002"], + ... description='Description of Add_CBI_Profile', + ... security_controls={ + ... "document_viewer": True, + ... "allow_printing": True, + ... "watermark": { + ... "enabled": True, + ... "show_user_id": True, + ... "show_timestamp": True, + ... "show_message": True, + ... "message": "Confidential" + ... }, + ... "flattened_pdf": False, + ... "upload_download": "all", + ... "restrict_keystrokes": True, + ... "copy_paste": "all", + ... "local_render": True + ... }, + ... debug_mode={ + ... "allowed": True, + ... "file_password": "" + ... }, + ... user_experience={ + ... "forward_to_zia": { + ... "enabled": True, + ... "organization_id": "44772833", + ... "cloud_name": "example_cloud", + ... "pac_file_url": "https://pac.example_cloud/proxy.pac" + ... }, + ... "browser_in_browser": True, + ... "persist_isolation_bar": True, + ... "session_persistence": True + ... }, + ... banner_id="97f339f6-9f85-40fb-8b76-f62cdf8f795c" + ... ) + """ + payload = { + "name": name, + "region_ids": region_ids, + "certificate_ids": certificate_ids, + } + + # Add optional parameters to payload if provided + for key, value in kwargs.items(): + if value is not None: + payload[key] = value # Keep in snake_case initially + + # Convert the entire payload from snake_case to camelCase + payload = recursive_snake_to_camel(payload) + + response = self.rest.post("profiles", json=payload, api_version="cbiconfig_v1") + if isinstance(response, Response) and response.status_code != 200: + # Assume non-200 responses indicate failure + raise Exception(f"API call failed with status {response.status_code}: {response.json()}") + return response + + def update_cbi_profile(self, profile_id: str, **kwargs) -> Box: + """ + Updates an existing cloud browser isolation profile. + + Args: + profile_id (str): + The unique identifier for the cloud browser isolation profile to be updated. + **kwargs: Optional keyword args. + + Keyword Args: + description (str, optional): A brief description of the security profile. + is_default (bool, optional): Indicates if this profile should be set as the default for new users. + banner_id (str, optional): The unique identifier for a custom banner displayed in the isolation session. + security_controls (dict, optional): Specifies the cloud browser isolation security settings. + + - document_viewer (bool): Enable or disable document viewing capabilities + - allow_printing (bool): Allow or restrict printing of documents + - watermark (dict): Configuration for watermarking documents displayed in the browser: + - enabled (bool): Enable or disable watermarking + - show_user_id (bool): Display user ID on the watermark. + - show_timestamp (bool): Include a timestamp in the watermark. + - show_message (bool): Include a custom message in the watermark. + - message (str): The custom message to display if 'show_message' is True. + + - flattened_pdf (bool): Specify whether PDFs should be flattened. + - upload_download (str): Control upload and download capabilities ('all', 'none', or other configurations). + - restrict_keystrokes (bool): Restrict the use of keystrokes within the isolation session. + - copy_paste (str): Control copy and paste capabilities ('all', 'none', or specific configurations). + - local_render (bool): Enable or disable local rendering of web content. + + debug_mode (dict, optional): Debug mode settings that may include logging and error tracking configurations. + + - allowed (bool, optional): Allow debug mode + - file_password (str, Optional): Optional password to debug files when this mode is enabled. + + user_experience (dict, optional): Settings that affect how end-users interact with the isolated browser. + + - forward_to_zia (dict): Configuration for forwarding traffic to ZIA: + - enabled (bool): Enable or disable forwarding. + - organization_id (str): Organization ID to use for forwarding. + - cloud_name (str): Name of the Zscaler cloud. + - pac_file_url (str): URL to the PAC file. + + - browser_in_browser (bool): Enable or disable the use of a browser within the isolated browser. + - persist_isolation_bar (bool): Specify whether the isolation bar should remain visible. + - session_persistence (bool): Enable or disable session persistence across browser restarts. + + Returns: + :obj:`Box`: The resource record for the updated cloud browser isolation profile. + + Examples: + Updating the name and description of a cloud browser isolation profile: + + >>> zpa.isolation.update_cbi_profile( + ... profile_id='1beed6be-eb22-4328-92f2-fbe73fd6e5c7', + ... name='CBI_Profile_Update' + ... description='CBI_Profile_Update' + ) + """ + current_profile_data = self.get_cbi_profile(profile_id) + if isinstance(current_profile_data, Response): + raise Exception(f"Failed to retrieve profile with ID {profile_id}: {current_profile_data.json()}") + + for key, value in kwargs.items(): + current_profile_data[key] = value + updated_payload = recursive_snake_to_camel(current_profile_data) + + response = self.rest.put(f"profiles/{profile_id}", json=updated_payload, api_version="cbiconfig_v1") + if isinstance(response, Response) and response.status_code != 200: + raise Exception(f"API call failed with status {response.status_code}: {response.json()}") + return self.get_cbi_profile(profile_id) + + def delete_cbi_profile(self, profile_id: str) -> int: + """ + Deletes the specified cloud browser isolation profile. + + Args: + profile_id (str): + The unique identifier for the cloud browser isolation profile to be deleted. + + Returns: + :obj:`int`: The response code for the operation. + + Examples: + >>> zpa.isolation.delete_cbi_profile('99999') + + """ + return self.rest.delete(f"profiles/{profile_id}", api_version="cbiconfig_v1").status_code + + def list_zpa_profiles(self, show_disabled=None, scope_id=None) -> Box: + """ + Returns a list of all cloud browser isolation zpa profiles, with options to filter by disabled status and scope. + + Args: + show_disabled (bool, optional): If set to True, the response includes disabled profiles. + scope_id (str, optional): The unique identifier of the scope of the tenant to filter the profiles. + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation zpa profiles. + + Examples: + >>> pprint(zpa.isolation.list_zpa_profiles()) + >>> pprint(zpa.isolation.list_zpa_profiles(show_disabled=True, scope_id="abc123")) + + """ + params = {} + if show_disabled is not None: + params["showDisabled"] = show_disabled + if scope_id is not None: + params["scopeId"] = scope_id + + return self.rest.get("zpaprofiles", params=params, api_version="cbiconfig_v1") + + def get_zpa_profile(self, cbi_profile_id: str) -> Box: + """ + Returns information on the specified cloud browser isolation profile based on the cbi_profile_id. + + Args: + cbi_profile_id (str): + The unique identifier for the cloud browser isolation profile. + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation profile. + + Examples: + >>> pprint(zpa.isolation.get_zpa_profile('055e0730-cb6b-486f-804d-448024d22d91')) + + """ + profiles = self.list_zpa_profiles() + for profile in profiles: + if profile.get("cbi_profile_id") == cbi_profile_id: + return Box(profile) + raise ValueError("Profile with ID {} not found".format(cbi_profile_id)) + + def list_regions(self: str) -> Box: + """ + Returns information a list of all cloud browser isolation regions. + + Args: + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation regions. + + Examples: + >>> pprint(zpa.isolation.list_regions()) + + """ + return self.rest.get("regions", api_version="cbiconfig_v1") + + def get_region(self, region_id: str) -> Box: + """ + Returns information on the specified cloud browser isolation region by ID. + + Args: + region_id (str): The unique identifier for the cloud browser isolation region. + + Returns: + :obj:`Box`: The resource record for the cloud browser isolation region. + + Examples: + >>> pprint(zpa.isolation.get_region('dc75dc8d-a713-49aa-821e-eb35da523cc2')) + + """ + regions = self.list_regions() + for region in regions: + if region["id"] == region_id: + return Box(region) + raise ValueError(f"Region with ID {region_id} not found") diff --git a/zscaler/zpa/isolation_profile.py b/zscaler/zpa/isolation_profile.py deleted file mode 100644 index 9c0939d4..00000000 --- a/zscaler/zpa/isolation_profile.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2023, Zscaler Inc. -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -from box import BoxList - -from zscaler.zpa.client import ZPAClient - - -class IsolationProfileAPI: - def __init__(self, client: ZPAClient): - self.rest = client - - def list_profiles(self, **kwargs) -> BoxList: - """ - Returns a list of all configured isolation profiles. - - Keyword Args: - max_items (int): The maximum number of items to request before stopping iteration. - max_pages (int): The maximum number of pages to request before stopping iteration. - pagesize (int): Specifies the page size. The default size is 20, but the maximum size is 500. - search (str, optional): The search string used to match against features and fields. - - Returns: - BoxList: A list of all configured isolation profiles. - - Examples: - >>> for isolation_profile in zpa.isolation_profiles.list_profiles(): - ... pprint(isolation_profile) - """ - list, _ = self.rest.get_paginated_data(path="/isolation/profiles", **kwargs) - return list - - def get_profile_by_name(self, name: str): - """ - Retrieves a specific isolation profile by its name. - - Args: - name (str): The name of the isolation profile to search for. - - Returns: - dict or None: The isolation profile with the specified name if found, otherwise None. - - Examples: - >>> profile = zpa.isolation_profiles.get_profile_by_name('DefaultProfile') - >>> print(profile) - """ - profiles = self.list_profiles() - for profile in profiles: - if profile.get("name") == name: - return profile - return None - - def get_profile_by_id(self, profile_id: str): - """ - Retrieves a specific isolation profile by its unique identifier (ID). - - Args: - profile_id (str): The ID of the isolation profile to retrieve. - - Returns: - dict or None: The isolation profile with the specified ID if found, otherwise None. - - Examples: - >>> profile = zpa.isolation_profiles.get_profile_by_id('12345') - >>> print(profile) - """ - profiles = self.list_profiles() - for profile in profiles: - if str(profile.get("id")) == str(profile_id): # Ensuring ID comparison as strings - return profile - return None diff --git a/zscaler/zpa/lss.py b/zscaler/zpa/lss.py index b8a582fd..476c1559 100644 --- a/zscaler/zpa/lss.py +++ b/zscaler/zpa/lss.py @@ -48,20 +48,20 @@ def _create_policy(self, conditions: list) -> list: conditions (list): List of condition tuples. Returns: - :obj:`dict`: Dictionary containing the LSS Log Receiver Policy conditions template. + :obj:`list`: List containing the LSS Log Receiver Policy conditions template. """ template = [] for condition in conditions: - # Template for SAML Policy Rule objects - if isinstance(condition, tuple) and len(condition) == 2 and condition[0] == "saml": - operand = {"operands": [{"objectType": "SAML", "entryValues": []}]} - for item in condition[1]: + # Template for SAML, SCIM, and SCIM_GROUP Policy Rule objects + if condition[0] in ["saml", "scim", "scim_group"]: + operand = {"operands": [{"objectType": condition[0].upper(), "entryValues": []}]} + for entry in condition[1]: entry_values = { - "lhs": item[0], - "rhs": item[1], + "lhs": entry[0], + "rhs": entry[1], } operand["operands"][0]["entryValues"].append(entry_values) # Template for client_type Policy Rule objects @@ -149,29 +149,24 @@ def list_configs(self, **kwargs) -> BoxList: list, _ = self.rest.get_paginated_data(path="/lssConfig", **kwargs, api_version="v2") return list - def get_config(self, lss_id: str) -> Box: + def get_config(self, lss_config_id: str) -> Box: """ Returns information on the specified LSS Receiver config. Args: - lss_id (str): + lss_config_id (str): The unique identifier for the LSS Receiver config. Returns: - :obj:`Box`: The resource record for the LSS Receiver config. + :obj:`Box`: The resource record for the LSS Receiver config in a Box object for easy attribute access. Examples: Print information on the specified LSS Receiver config. >>> print(zpa.lss.get_config('99999')) - """ - response = self.rest.send("GET", "/lssConfig/%s" % (lss_id), api_version="v2") - if isinstance(response, Response): - status_code = response.status_code - if status_code != 200: - return None - return response + # Perform the GET request + return self.rest.get(f"lssConfig/{lss_config_id}", api_version="v2") def get_log_formats(self, log_type=None) -> Box: """ @@ -208,10 +203,7 @@ def get_log_formats(self, log_type=None) -> Box: def get_status_codes(self, log_type: str = "all") -> Box: """ - Returns a list of LSS Session Status Codes. - - The LSS Session Status codes are used to filter the messages received by LSS. LSS Session Status Codes can be - used when adding or updating the filters for an LSS Log Receiver. + Returns a list of LSS Session Status Codes filtered by log type. Args: log_type (str): @@ -240,27 +232,23 @@ def get_status_codes(self, log_type: str = "all") -> Box: ... print(item) """ - path = "/statusCodes" - if log_type != "all": - if log_type in [ - "user_activity", - "user_status", - "private_svc_edge_status", - "app_connector_status", - ]: - path = f"{path}/{self.source_log_map[log_type]}" - else: - raise ValueError("Incorrect log_type provided.") - - full_url = f"{self.v2_admin_url}{path}" + full_url = f"{self.v2_admin_url}/statusCodes" response = requests.get(full_url, headers=self.rest.headers) + response.raise_for_status() + all_status_codes = response.json() - if response.status_code == 200: - # Assuming that the response is a JSON object that needs to be converted to a Box - response_data = Box(response.json()) - return response_data if log_type == "all" else response_data[log_type] + if log_type == "all": + return Box(all_status_codes) else: - response.raise_for_status() + filtered_status_codes = {} + log_type_key = self.source_log_map.get(log_type) + if log_type_key: + for code, details in all_status_codes.items(): + if log_type_key in details.get("log_types", []): + filtered_status_codes[code] = details + return Box(filtered_status_codes) + else: + raise ValueError("Incorrect log_type provided.") def add_lss_config( self, @@ -485,7 +473,7 @@ def update_lss_config(self, lss_config_id: str, **kwargs): .. code-block:: python - zpa.lss.update_config( + zpa.lss.update_lss_config( name="user_status_to_siem", policy_rules=[ ("idp", ["idp_id"]), @@ -518,7 +506,7 @@ def update_lss_config(self, lss_config_id: str, **kwargs): if keys_exists(payload, "policyRuleResource", "name"): policy_name = payload["policyRuleResource"]["name"] else: - policy_name = "SIEM_POLICY" + policy_name = "placeholder" payload["policyRuleResource"] = { "conditions": self._create_policy(kwargs.pop("policy_rules")), "name": kwargs.pop("policy_name", policy_name), @@ -528,18 +516,20 @@ def update_lss_config(self, lss_config_id: str, **kwargs): for key, value in kwargs.items(): payload[snake_to_camel(key)] = value - resp = self.rest.put(f"/lssConfig/{lss_config_id}", api_version="v2", json=payload).status_code - - # Return the object if it was updated successfully - if not isinstance(resp, Response): + # Send the update request to the API + resp = self.rest.put(f"/lssConfig/{lss_config_id}", api_version="v2", json=payload) + if resp.status_code == 204: + # Fetch and return the updated configuration as no content is returned with a 204 response return self.get_config(lss_config_id) + else: + raise Exception("Failed to update LSS Config, status code: {}".format(resp.status_code)) - def delete_lss_config(self, lss_id: str) -> int: + def delete_lss_config(self, lss_config_id: str) -> int: """ Delete the specified LSS Receiver Config. Args: - lss_id (str): The unique identifier for the LSS Receiver Config to be deleted. + lss_config_id (str): The unique identifier for the LSS Receiver Config to be deleted. Returns: :obj:`int`: @@ -548,7 +538,7 @@ def delete_lss_config(self, lss_id: str) -> int: Examples: Delete an LSS Receiver config. - >>> zpa.lss.delete_config('99999') + >>> zpa.lss.delete_lss_config('99999') """ - return self.rest.delete(f"/lssConfig/{lss_id}", api_version="v2").status_code + return self.rest.delete(f"/lssConfig/{lss_config_id}", api_version="v2").status_code diff --git a/zscaler/zpa/posture_profiles.py b/zscaler/zpa/posture_profiles.py index e3a9957a..aeedd480 100644 --- a/zscaler/zpa/posture_profiles.py +++ b/zscaler/zpa/posture_profiles.py @@ -52,6 +52,25 @@ def list_profiles(self, **kwargs) -> BoxList: return list def get_profile_by_name(self, name): + """ + Searches for and returns a posture profile based on its name. + + This method performs a case-sensitive search through all posture profiles, + returning the first profile that matches the specified name exactly. + + Args: + name (str): The name of the posture profile to search for. + + Returns: + Box: The posture profile that matches the given name, or None if no match is found. + + Examples: + >>> profile = zpa.posture_profiles.get_profile_by_name("Example Profile Name") + >>> if profile: + ... print("Profile ID:", profile.id) + ... else: + ... print("Profile not found.") + """ profiles = self.list_profiles() for profile in profiles: if profile.get("name") == name: @@ -84,14 +103,26 @@ def get_udid_by_profile_name(self, search_name: str, **kwargs) -> str: """ Searches for a posture profile by name and returns its posture_udid. + This function searches through all configured posture profiles, comparing the + provided search_name against each profile's name, both exactly and with any cloud suffix removed. + It returns the 'posture_udid' of the first matching profile found. + Args: search_name (str): The name of the posture profile to search for. Keyword Args: - **kwargs: Additional keyword arguments to pass to the list_profiles method. + **kwargs: Additional keyword arguments to pass to the list_profiles method, such as + 'max_items', 'max_pages', 'pagesize', and 'search'. Returns: str: The posture_udid of the found posture profile, or None if not found. + + Examples: + >>> udid = zpa.posture_profiles.get_udid_by_profile_name("Example Profile") + >>> if udid: + ... print(f"Found Profile UDID: {udid}") + ... else: + ... print("Profile not found.") """ profiles = self.list_profiles(**kwargs) for profile in profiles: diff --git a/zscaler/zpa/scim_groups.py b/zscaler/zpa/scim_groups.py index 91c95f23..db718c27 100644 --- a/zscaler/zpa/scim_groups.py +++ b/zscaler/zpa/scim_groups.py @@ -16,7 +16,7 @@ from box import Box, BoxList - +import time from zscaler.zpa.client import ZPAClient @@ -94,40 +94,3 @@ def get_group(self, group_id: str, **kwargs) -> Box: """ response = self.rest.get(f"/scimgroup/{group_id}", **kwargs, api_version="userconfig_v1") return response - - def search_group(self, idp_id: str, group_name: str, **kwargs) -> dict: - """ - Searches and returns the SCIM group with the specified name for the given IdP. - """ - page_size = kwargs.get("pagesize", 500) # Adjust the page size as needed - - # Calculate the total pages using a synchronous call - total_pages = 1 - page_number = 1 - - # Loop over each page to search for the group - while True: - page = self._get_page(idp_id, page_number, group_name, page_size) - total_pages = int(page.get("total_pages", "0")) - for group in page.get("list", []): - if group.get("name") == group_name: - return group # Return the found group immediately - if page_number >= total_pages: - break - page_number = page_number + 1 - return None # Return None if the group wasn't found - - def _get_page(self, idp_id, page_number, search, page_size): - params = { - "page": page_number, - "search": search, - "pagesize": page_size, - "sortBy": "name", - "sortOrder": "DSC", - } - page = self.rest.get( - path=f"/scimgroup/idpId/{idp_id}", - params=params, - api_version="userconfig_v1", - ) - return page diff --git a/zscaler/zpa/server_groups.py b/zscaler/zpa/server_groups.py index 236ac3a3..dfc3ba60 100644 --- a/zscaler/zpa/server_groups.py +++ b/zscaler/zpa/server_groups.py @@ -71,7 +71,6 @@ def get_group(self, group_id: str) -> Box: >>> pprint(zpa.server_groups.get_group('99999')) """ - return self.rest.get(f"serverGroup/{group_id}") def get_server_group_by_name(self, name): diff --git a/zscaler/zpa/trusted_networks.py b/zscaler/zpa/trusted_networks.py index 4549462e..75e35b74 100644 --- a/zscaler/zpa/trusted_networks.py +++ b/zscaler/zpa/trusted_networks.py @@ -81,45 +81,25 @@ def get_network(self, network_id: str) -> Box: return None return response - def get_by_network_id(self, network_id: str, **kwargs) -> Union[Box, None]: + def get_network_udid(self, network_udid: str) -> Box: """ - Returns the trusted network based on the networkId. + Returns a trusted network based on its 'network_id'. Args: - network_id (str): The unique Network ID for the network ID. - - Keyword Args: - **max_items (int): The maximum number of items to request before stopping iteration. - **max_pages (int): The maximum number of pages to request before stopping iteration. - **pagesize (int): Specifies the page size. The default size is 100, but the maximum size is 500. - **search (str, optional): The search string used to match against features and fields. + network_udid (str): The unique identifier for the network_id of the trusted network. Returns: - Union[Box, None]: The resource record for the trusted networks. - """ - - page = 0 - page_size = kwargs.get("pagesize", 100) # default page size changed to 100 - max_pages = kwargs.get("max_pages", None) - - while True: - params = { - "pagesize": page_size, - "page": page, - "search": network_id, # use the search parameter if supported - **kwargs, - } - networks = self.list_networks(**params) - - if not networks: - break # exit if no more networks - - for network in networks: - if network.get("networkId") == network_id: - return Box(network) - - page += 1 - if max_pages and page >= max_pages: - break + :obj:`Box`: The resource record for the trusted network, or None if not found. + Examples: + >>> network = zpa.trusted_networks.get_network_udid('9432db25-b80b-4b9a-b2e1-e30c67412593') + >>> if network: + ... print("Network found:", network) + ... else: + ... print("No network found with the given network_id") + """ + networks = self.list_networks() + for network in networks: + if network.get("network_id") == network_udid: + return network return None