├── src └── czds │ ├── py.typed │ ├── __init__.py │ ├── __main__.py │ ├── models.py │ ├── exceptions.py │ ├── czds.py │ ├── logger.py │ ├── base.py │ ├── configuration.py │ └── connector.py ├── .gitattributes ├── .coveragerc ├── .darglint ├── docs ├── codeofconduct.md ├── requirements.txt ├── license.md ├── reference.md ├── usage.md ├── contributing.md ├── conf.py └── index.md ├── .github ├── workflows │ ├── constraints.txt │ ├── labeler.yml │ ├── distribute.yml │ ├── release.yml │ └── tests.yml ├── dependabot.yml ├── release-drafter.yml └── labels.yml ├── codecov.yml ├── tests ├── test_connector.py ├── conftest.py ├── test_czds.py └── test_base.py ├── .gitignore ├── .readthedocs.yml ├── .flake8 ├── .cookiecutter.json ├── SECURITY.md ├── LICENSE.md ├── .pre-commit-config.yaml ├── pyproject.toml ├── CONTRIBUTING.md ├── CHANGELOG.md ├── README.md ├── CODE_OF_CONDUCT.md └── noxfile.py /src/czds/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | fail_under = 50 3 | -------------------------------------------------------------------------------- /.darglint: -------------------------------------------------------------------------------- 1 | [darglint] 2 | strictness = long 3 | -------------------------------------------------------------------------------- /docs/codeofconduct.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CODE_OF_CONDUCT.md 2 | 3 | ``` 4 | -------------------------------------------------------------------------------- /src/czds/__init__.py: -------------------------------------------------------------------------------- 1 | """CZDS.""" 2 | 3 | from .czds import CZDS 4 | 5 | 6 | __all__ = ["CZDS"] 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | furo==2022.12.7 2 | sphinx==6.1.3 3 | sphinx-click==4.4.0 4 | myst_parser==0.18.1 5 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ```{literalinclude} ../LICENSE 4 | --- 5 | language: none 6 | --- 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## czds 4 | 5 | ```{eval-rst} 6 | .. automodule:: czds 7 | :members: 8 | ``` 9 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```{eval-rst} 4 | .. click:: czds.__main__:main 5 | :prog: czds 6 | :nested: full 7 | ``` 8 | -------------------------------------------------------------------------------- /.github/workflows/constraints.txt: -------------------------------------------------------------------------------- 1 | pip==25.1.1 2 | nox==2023.4.22 3 | nox-poetry==1.0.2 4 | poetry==1.4.2 5 | virtualenv==20.19.0 6 | pre-commit==3.3.1 7 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ```{include} ../CONTRIBUTING.md 2 | --- 3 | end-before: 4 | --- 5 | ``` 6 | 7 | [code of conduct]: codeofconduct 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: "50" 7 | patch: 8 | default: 9 | target: "50" 10 | -------------------------------------------------------------------------------- /tests/test_connector.py: -------------------------------------------------------------------------------- 1 | """Tests the czds.connector module classes.""" 2 | 3 | 4 | def test_connector(main_class): 5 | """Tests the creation of a Connector class.""" 6 | assert True 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache/ 2 | /.coverage 3 | /.coverage.* 4 | /.nox/ 5 | /.python-version 6 | /.pytype/ 7 | /dist/ 8 | /docs/_build/ 9 | /src/*.egg-info/ 10 | __pycache__/ 11 | 12 | .DS_Store 13 | *.gz 14 | *.log 15 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-20.04 4 | tools: 5 | python: "3.10" 6 | sphinx: 7 | configuration: docs/conf.py 8 | formats: all 9 | python: 10 | install: 11 | - requirements: docs/requirements.txt 12 | - path: . 13 | -------------------------------------------------------------------------------- /src/czds/__main__.py: -------------------------------------------------------------------------------- 1 | """Command-line interface.""" 2 | 3 | import fire 4 | 5 | from .czds import CZDS 6 | 7 | 8 | def main(): 9 | """Main entry point for the command line interface of CZDS.""" 10 | fire.Fire(CZDS) 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = B,B9,C,D,DAR,E,F,N,RST,S,W 3 | ignore = E203,E501,RST201,RST203,RST301,W503,B907,B904,F841 4 | max-line-length = 120 5 | max-complexity = 10 6 | docstring-convention = google 7 | per-file-ignores = tests/*:S101 8 | rst-roles = class,const,func,meth,mod,ref 9 | rst-directives = deprecated 10 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx configuration.""" 2 | 3 | project = "CZDS" 4 | author = "Josh Rickard" 5 | copyright = "2023, Josh Rickard" 6 | extensions = [ 7 | "sphinx.ext.autodoc", 8 | "sphinx.ext.napoleon", 9 | "sphinx_click", 10 | "myst_parser", 11 | ] 12 | autodoc_typehints = "description" 13 | html_theme = "furo" 14 | -------------------------------------------------------------------------------- /src/czds/models.py: -------------------------------------------------------------------------------- 1 | """Contains data models around CZDS.""" 2 | 3 | from typing import AnyStr 4 | 5 | from attrs import define 6 | 7 | 8 | @define 9 | class ZoneData: 10 | """Data model for each zone data element.""" 11 | 12 | zone_name: AnyStr 13 | dns_record: AnyStr 14 | ttl: AnyStr 15 | record_class: AnyStr 16 | record_type: AnyStr 17 | record_data: AnyStr 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | --- 3 | end-before: 4 | --- 5 | ``` 6 | 7 | [license]: license 8 | [contributor guide]: contributing 9 | [command-line reference]: usage 10 | 11 | ```{toctree} 12 | --- 13 | hidden: 14 | maxdepth: 1 15 | --- 16 | 17 | usage 18 | reference 19 | contributing 20 | Code of Conduct 21 | License 22 | Changelog 23 | ``` 24 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Configuration for the pytest test suite.""" 2 | 3 | import pytest 4 | 5 | from czds import CZDS 6 | 7 | # from czds import CZDS 8 | from czds.__main__ import main 9 | 10 | 11 | @pytest.fixture 12 | def main_class() -> CZDS: 13 | """Fixture for the main CZDS class interface.""" 14 | return CZDS 15 | 16 | 17 | @pytest.fixture 18 | def runner(): 19 | """Fixture for invoking command-line interfaces.""" 20 | return main() 21 | -------------------------------------------------------------------------------- /tests/test_czds.py: -------------------------------------------------------------------------------- 1 | """Testing the main CZDS class.""" 2 | 3 | 4 | def test_czds_base_variables(main_class): 5 | """Testing to ensure that the CZDS parameters are stored correctly.""" 6 | from czds.base import Base 7 | 8 | data = {"username": "test.name@nothing.xyz", "password": "testpassword1!", "save_directory": "./"} 9 | main_class(**data) 10 | 11 | assert Base.USERNAME == data["username"] 12 | assert Base.PASSWORD == data["password"] 13 | assert Base.SAVE_PATH == data["save_directory"] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: pip 8 | directory: "/.github/workflows" 9 | schedule: 10 | interval: daily 11 | - package-ecosystem: pip 12 | directory: "/docs" 13 | schedule: 14 | interval: daily 15 | - package-ecosystem: pip 16 | directory: "/" 17 | schedule: 18 | interval: daily 19 | versioning-strategy: lockfile-only 20 | allow: 21 | - dependency-type: "all" 22 | -------------------------------------------------------------------------------- /.cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copy_without_render": [ 3 | ".github/workflows/tests.yml", 4 | ".github/workflows/distribute.yml" 5 | ], 6 | "_output_dir": "/Users/voltron/_GitHub", 7 | "_template": "gh:MSAdministrator/cookiecutter-hypermodern-python", 8 | "author": "Josh Rickard", 9 | "copyright_year": "2023", 10 | "development_status": "Development Status :: 3 - Alpha", 11 | "email": "rickardja@live.com", 12 | "friendly_name": "CZDS", 13 | "github_user": "MSAdministrator", 14 | "license": "MIT", 15 | "package_name": "czds", 16 | "project_name": "czds", 17 | "version": "0.0.1" 18 | } 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # 🔐 Security Policy 2 | 3 | > Do not open issues that might have security implications! 4 | 5 | > It is critical that security related issues are reported privately so we have time to address them before they become public knowledge. 6 | 7 | Vulnerabilities can be reported by emailing core members: 8 | 9 | - Josh Rickard [rickardja@live.com](mailto:rickardja@live.com) 10 | 11 | Be sure to include as much detail as necessary in your report. As with reporting normal issues, a minimal reproducible example will help the maintainers address the issue faster. If you are able, you may also include a fix for the issue generated with git format-patch. 12 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | paths: 8 | - '.github/labels.yml' 9 | - '.github/workflows/labels.yml' 10 | 11 | jobs: 12 | labeler: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | issues: write # <--- permission granted explicitely 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v4 20 | - 21 | name: Run Labeler 22 | if: success() 23 | uses: crazy-max/ghaction-github-labeler@v5 24 | with: 25 | github-token: ${{ secrets.GITHUB_TOKEN }} 26 | yaml-file: .github/labels.yml 27 | skip-delete: true 28 | dry-run: false 29 | -------------------------------------------------------------------------------- /.github/workflows/distribute.yml: -------------------------------------------------------------------------------- 1 | name: Distribute Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | deploy: 9 | name: Distribute 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Check out the repository 13 | uses: actions/checkout@v4 14 | - name: Set up Python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.10" 18 | - name: Set up poetry 19 | uses: abatilo/actions-poetry@v2.2.0 20 | with: 21 | poetry-version: 1.3.2 22 | - name: Publish 23 | run: | 24 | poetry config http-basic.pypi ${{ secrets.PYPI_USERNAME }} ${{ secrets.PYPI_PASSWORD }} 25 | poetry publish --build 26 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """Tests for the base class.""" 2 | 3 | 4 | def test_threads(main_class): 5 | """Tests caluclation of threads.""" 6 | import os 7 | 8 | from czds.base import Base 9 | 10 | assert Base().THREAD_COUNT == os.cpu_count() * 5 11 | 12 | 13 | def test_chunk(main_class): 14 | """Tests chunk method.""" 15 | from czds.base import Base 16 | 17 | sample_list = ["1", "2", "3", "4"] 18 | chunked_lists = Base()._chunk(items=sample_list, chunk_size=1) 19 | for item in chunked_lists: 20 | assert isinstance(item, list) 21 | 22 | 23 | def test_run_threaded(main_class): 24 | """Tests the run_threaded method in Base.""" 25 | assert True 26 | 27 | 28 | def test_log(main_class): 29 | """Tests the log method in Base.""" 30 | assert True 31 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: ":boom: Breaking Changes" 3 | label: "breaking" 4 | - title: ":rocket: Features" 5 | label: "enhancement" 6 | - title: ":fire: Removals and Deprecations" 7 | label: "removal" 8 | - title: ":beetle: Fixes" 9 | label: "bug" 10 | - title: ":racehorse: Performance" 11 | label: "performance" 12 | - title: ":rotating_light: Testing" 13 | label: "testing" 14 | - title: ":construction_worker: Continuous Integration" 15 | label: "ci" 16 | - title: ":books: Documentation" 17 | label: "documentation" 18 | - title: ":hammer: Refactoring" 19 | label: "refactoring" 20 | - title: ":lipstick: Style" 21 | label: "style" 22 | - title: ":package: Dependencies" 23 | labels: 24 | - "dependencies" 25 | - "build" 26 | template: | 27 | ## Changes 28 | 29 | $CHANGES 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2023 Josh Rickard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/czds/exceptions.py: -------------------------------------------------------------------------------- 1 | """czds.utils.exceptions.""" 2 | 3 | from requests.exceptions import HTTPError 4 | 5 | 6 | class CZDSConnectionError(HTTPError): 7 | """Base class for all requests HTTPErrors.""" 8 | 9 | def __init__(self, status_code: int, name: str, http_error: HTTPError) -> None: 10 | """Base exception for CZDS HTTP requests. 11 | 12 | Args: 13 | status_code (int): The status code received from the request. 14 | name (str): The name of the error. 15 | http_error (HTTPError): The HTTP Error from the requests response. 16 | """ 17 | from .base import Base 18 | 19 | Base().log(message=f"\n{name} Error Occurred.\nStatus Code: {status_code}\nError: {http_error}\n") 20 | 21 | 22 | class UnsupportedTypeError(TypeError): 23 | """Raised when the wrong type is provided.""" 24 | 25 | def __init__(self, *args: object) -> None: 26 | """Wrapper for TypeError exception class which passed arguments along to TypeError.""" 27 | super().__init__(*args) 28 | 29 | 30 | class CustomExceptionError(Exception): 31 | """Raised when an error is encountered.""" 32 | 33 | def __init__(self, value: str) -> None: 34 | """Raises when the value is unknown.""" 35 | pass 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | environment: pypi 15 | permissions: 16 | # IMPORTANT: this permission is mandatory for Trusted Publishing 17 | id-token: write 18 | steps: 19 | - uses: googleapis/release-please-action@v4 20 | id: release 21 | with: 22 | release-type: python 23 | # The logic below handles the PyPi distribution: 24 | - uses: actions/checkout@v4 25 | # these if statements ensure that a publication only occurs when 26 | # a new release is created: 27 | if: ${{ steps.release.outputs.releases_created }} 28 | - name: Set up Python 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: "3.10" 32 | if: ${{ steps.release.outputs.releases_created }} 33 | - name: Set up poetry 34 | uses: abatilo/actions-poetry@v2.2.0 35 | with: 36 | poetry-version: 1.4.2 37 | if: ${{ steps.release.outputs.releases_created }} 38 | - name: Build tarball 39 | run: | 40 | poetry build 41 | if: ${{ steps.release.outputs.releases_created }} 42 | - name: Publish package distributions to PyPI 43 | uses: pypa/gh-action-pypi-publish@release/v1 44 | if: ${{ steps.release.outputs.releases_created }} 45 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: black 5 | name: black 6 | entry: black 7 | args: [./src/czds, --line-length, "120"] 8 | language: system 9 | types: [python] 10 | require_serial: true 11 | - id: check-added-large-files 12 | name: Check for added large files 13 | entry: check-added-large-files 14 | language: system 15 | - id: check-toml 16 | name: Check Toml 17 | entry: check-toml 18 | language: system 19 | types: [toml] 20 | - id: check-yaml 21 | name: Check Yaml 22 | entry: check-yaml 23 | language: system 24 | types: [yaml] 25 | - id: darglint 26 | name: darglint 27 | entry: darglint 28 | language: system 29 | types: [python] 30 | stages: [manual] 31 | - id: end-of-file-fixer 32 | name: Fix End of Files 33 | entry: end-of-file-fixer 34 | language: system 35 | types: [text] 36 | stages: [pre-commit, pre-push, manual] 37 | - id: flake8 38 | name: flake8 39 | entry: flake8 40 | language: system 41 | types: [python] 42 | require_serial: true 43 | args: ["./src/czds"] 44 | - id: isort 45 | name: isort 46 | entry: isort 47 | require_serial: true 48 | language: system 49 | types_or: [cython, pyi, python] 50 | args: ["./src/czds"] 51 | - id: pyupgrade 52 | name: pyupgrade 53 | description: Automatically upgrade syntax for newer versions. 54 | entry: pyupgrade 55 | language: system 56 | types: [python] 57 | args: [--py37-plus] 58 | - id: trailing-whitespace 59 | name: Trim Trailing Whitespace 60 | entry: trailing-whitespace-fixer 61 | language: system 62 | types: [text] 63 | stages: [pre-commit, pre-push, manual] 64 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: bfd4f2 10 | - name: bug 11 | description: Something isn't working 12 | color: d73a4a 13 | - name: build 14 | description: Build System and Dependencies 15 | color: bfdadc 16 | - name: ci 17 | description: Continuous Integration 18 | color: 4a97d6 19 | - name: dependencies 20 | description: Pull requests that update a dependency file 21 | color: 0366d6 22 | - name: documentation 23 | description: Improvements or additions to documentation 24 | color: 0075ca 25 | - name: duplicate 26 | description: This issue or pull request already exists 27 | color: cfd3d7 28 | - name: enhancement 29 | description: New feature or request 30 | color: a2eeef 31 | - name: github_actions 32 | description: Pull requests that update Github_actions code 33 | color: "000000" 34 | - name: good first issue 35 | description: Good for newcomers 36 | color: 7057ff 37 | - name: help wanted 38 | description: Extra attention is needed 39 | color: 008672 40 | - name: invalid 41 | description: This doesn't seem right 42 | color: e4e669 43 | - name: performance 44 | description: Performance 45 | color: "016175" 46 | - name: python 47 | description: Pull requests that update Python code 48 | color: 2b67c6 49 | - name: question 50 | description: Further information is requested 51 | color: d876e3 52 | - name: refactoring 53 | description: Refactoring 54 | color: ef67c4 55 | - name: removal 56 | description: Removals and Deprecations 57 | color: 9ae7ea 58 | - name: style 59 | description: Style 60 | color: c120e5 61 | - name: testing 62 | description: Testing 63 | color: b1fc6f 64 | - name: wontfix 65 | description: This will not be worked on 66 | color: ffffff 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "czds" 3 | version = "0.1.5" 4 | packages = [ 5 | { include = "czds", from = "src" } 6 | ] 7 | description = "CZDS" 8 | authors = ["Josh Rickard "] 9 | maintainers = ["Josh Rickard "] 10 | license = "MIT" 11 | readme = "README.md" 12 | homepage = "https://github.com/MSAdministrator/czds" 13 | repository = "https://github.com/MSAdministrator/czds" 14 | keywords = [ 15 | "ICAAN", 16 | "CZDS", 17 | "DNS", 18 | "Security" 19 | ] 20 | classifiers = [ 21 | "Development Status :: 3 - Alpha", 22 | ] 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.10" 26 | requests = "^2.28.2" 27 | attrs = "^25.3.0" 28 | pyyaml = "^6.0" 29 | fire = "^0.7.0" 30 | click = ">=8.0.1" 31 | prompt-toolkit = "^3.0.36" 32 | rich = "^13.3.1" 33 | pytest = "^8.4.1" 34 | legacy-cgi = "^2.6.3" 35 | pre-commit = "^4.2.0" 36 | 37 | [tool.poetry.urls] 38 | Changelog = "https://github.com/MSAdministrator/czds/releases" 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | Pygments = ">=2.10.0" 42 | black = ">=21.10b0" 43 | coverage = {extras = ["toml"], version = ">=6.2"} 44 | darglint = ">=1.8.1" 45 | flake8 = ">=4.0.1" 46 | flake8-bandit = ">=2.1.2" 47 | flake8-bugbear = ">=21.9.2" 48 | flake8-docstrings = ">=1.6.0" 49 | flake8-rst-docstrings = ">=0.3.0" 50 | furo = ">=2021.11.12" 51 | isort = ">=5.10.1" 52 | mypy = ">=0.930" 53 | pep8-naming = ">=0.12.1" 54 | pre-commit = ">=2.16.0" 55 | pre-commit-hooks = ">=4.4.0" 56 | pyupgrade = ">=2.29.1" 57 | safety = ">=1.10.3" 58 | sphinx = ">=4.3.2" 59 | sphinx-autobuild = ">=2021.3.14" 60 | sphinx-click = ">=3.0.2" 61 | typeguard = ">=2.13.3" 62 | xdoctest = {extras = ["colors"], version = ">=0.15.10"} 63 | myst-parser = {version = ">=0.16.1"} 64 | 65 | [tool.poetry.scripts] 66 | czds = "czds.__main__:main" 67 | 68 | [tool.coverage.paths] 69 | source = ["src", "*/site-packages"] 70 | tests = ["tests", "*/tests"] 71 | 72 | [tool.coverage.run] 73 | branch = true 74 | source = ["czds", "tests"] 75 | 76 | [tool.coverage.report] 77 | show_missing = true 78 | fail_under = 100 79 | 80 | [tool.commitizen] 81 | version = "0.0.1" 82 | version_files = [ 83 | "src/czds/__init__.py", 84 | "pyproject.toml:version" 85 | ] 86 | tag_format = "$major.$minor.$patch" 87 | 88 | [tool.isort] 89 | profile = "black" 90 | force_single_line = true 91 | lines_after_imports = 2 92 | 93 | [tool.mypy] 94 | strict = true 95 | warn_unreachable = true 96 | pretty = true 97 | show_column_numbers = true 98 | show_error_codes = true 99 | show_error_context = true 100 | 101 | [build-system] 102 | requires = ["poetry-core>=1.0.0"] 103 | build-backend = "poetry.core.masonry.api" 104 | -------------------------------------------------------------------------------- /src/czds/czds.py: -------------------------------------------------------------------------------- 1 | """Main entrypoint for czds.""" 2 | 3 | from typing import AnyStr 4 | from typing import Dict 5 | from typing import List 6 | 7 | from .base import Base 8 | from .connector import CZDSConnector 9 | from .exceptions import CZDSConnectionError 10 | 11 | 12 | class CZDS(Base): 13 | """Main class for ICAAN CZDS.""" 14 | 15 | links: List[str] = [] 16 | 17 | def __init__(self, username: AnyStr, password: AnyStr, save_directory: AnyStr) -> None: 18 | """Sets the username and password for API authentication. 19 | 20 | Args: 21 | username (AnyStr): The username to access CZDS. 22 | password (AnyStr): The password to access CZDS. 23 | save_directory (AnyStr): The directory to save zone files to. 24 | """ 25 | Base.USERNAME = username 26 | Base.PASSWORD = password 27 | Base.SAVE_PATH = save_directory 28 | 29 | def list_links(self) -> List[str]: 30 | """Returns a list of all CZDS Zone Link urls. 31 | 32 | Raises: 33 | CZDSConnectionError: Raises connection errors. 34 | 35 | Returns: 36 | List[str]: A list CZDS Zone Link urls. 37 | """ 38 | try: 39 | self.connection = CZDSConnector() 40 | except CZDSConnectionError as cze: 41 | raise cze 42 | if not self.links: 43 | self.links = self.connection._get_zone_links() 44 | return self.links 45 | 46 | def get_zone( 47 | self, link: AnyStr = None, threaded: bool = False, output_format: AnyStr = None 48 | ) -> AnyStr or List[Dict[str, str]]: 49 | """Retrieves all or a single CZDS Zone File. 50 | 51 | If you DO NOT provide a link, we will retrieve all available link files from your account and return them. 52 | 53 | Args: 54 | link (AnyStr): A CZDS Zone Link URL. Defaults to None. 55 | threaded (bool): Whether or not to run multi-threaded. 56 | output_format (AnyStr): The output format for the parsed zone file. 57 | Accepts 'none','text' and 'json' at this time. Defaults to None. 58 | 59 | Raises: 60 | CZDSConnectionError: Raises connection errors. 61 | 62 | Returns: 63 | AnyStr or List[Dict[str, str]]: _description_ 64 | """ 65 | Base.OUTPUT_FORMAT = output_format 66 | return_list: List[Dict[str, str]] = [] 67 | try: 68 | self.connection = CZDSConnector() 69 | except CZDSConnectionError as cze: 70 | raise cze 71 | if link: 72 | return_list.append(self.connection.download(zone_file_list=link)) 73 | else: 74 | if threaded: 75 | return self.run_threaded(method=self.connection.download, list_data=self.list_links()) 76 | else: 77 | for link in self.list_links(): 78 | self.__logger.info(f"Downloading zone file from '{link}'.") 79 | return_list.append(self.connection.download(zone_file_list=link)) 80 | return return_list 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Guide 2 | 3 | Thank you for your interest in improving this project. 4 | This project is open-source under the [MIT license] and 5 | welcomes contributions in the form of bug reports, feature requests, and pull requests. 6 | 7 | Here is a list of important resources for contributors: 8 | 9 | - [Source Code] 10 | - [Documentation] 11 | - [Issue Tracker] 12 | - [Code of Conduct] 13 | 14 | [mit license]: https://opensource.org/licenses/MIT 15 | [source code]: https://github.com/MSAdministrator/czds 16 | [documentation]: https://czds.readthedocs.io/ 17 | [issue tracker]: https://github.com/MSAdministrator/czds/issues 18 | 19 | ## How to report a bug 20 | 21 | Report bugs on the [Issue Tracker]. 22 | 23 | When filing an issue, make sure to answer these questions: 24 | 25 | - Which operating system and Python version are you using? 26 | - Which version of this project are you using? 27 | - What did you do? 28 | - What did you expect to see? 29 | - What did you see instead? 30 | 31 | The best way to get your bug fixed is to provide a test case, 32 | and/or steps to reproduce the issue. 33 | 34 | ## How to request a feature 35 | 36 | Request features on the [Issue Tracker]. 37 | 38 | ## How to set up your development environment 39 | 40 | You need Python 3.7+ and the following tools: 41 | 42 | - [Poetry] 43 | - [Nox] 44 | - [nox-poetry] 45 | 46 | Install the package with development requirements: 47 | 48 | ```console 49 | $ poetry install 50 | ``` 51 | 52 | You can now run an interactive Python session, 53 | or the command-line interface: 54 | 55 | ```console 56 | $ poetry run python 57 | $ poetry run czds 58 | ``` 59 | 60 | [poetry]: https://python-poetry.org/ 61 | [nox]: https://nox.thea.codes/ 62 | [nox-poetry]: https://nox-poetry.readthedocs.io/ 63 | 64 | ## How to test the project 65 | 66 | Run the full test suite: 67 | 68 | ```console 69 | $ nox 70 | ``` 71 | 72 | List the available Nox sessions: 73 | 74 | ```console 75 | $ nox --list-sessions 76 | ``` 77 | 78 | You can also run a specific Nox session. 79 | For example, invoke the unit test suite like this: 80 | 81 | ```console 82 | $ nox --session=tests 83 | ``` 84 | 85 | Unit tests are located in the _tests_ directory, 86 | and are written using the [pytest] testing framework. 87 | 88 | [pytest]: https://pytest.readthedocs.io/ 89 | 90 | ## How to submit changes 91 | 92 | Open a [pull request] to submit changes to this project. 93 | 94 | Your pull request needs to meet the following guidelines for acceptance: 95 | 96 | - The Nox test suite must pass without errors and warnings. 97 | - Include unit tests. This project maintains 100% code coverage. 98 | - If your changes add functionality, update the documentation accordingly. 99 | 100 | Feel free to submit early, though—we can always iterate on this. 101 | 102 | To run linting and code formatting checks before committing your change, you can install pre-commit as a Git hook by running the following command: 103 | 104 | ```console 105 | $ nox --session=pre-commit -- install 106 | ``` 107 | 108 | It is recommended to open an issue before starting work on anything. 109 | This will allow a chance to talk it over with the owners and validate your approach. 110 | 111 | [pull request]: https://github.com/MSAdministrator/czds/pulls 112 | 113 | 114 | 115 | [code of conduct]: CODE_OF_CONDUCT.md 116 | -------------------------------------------------------------------------------- /src/czds/logger.py: -------------------------------------------------------------------------------- 1 | """Custom logger.""" 2 | 3 | import logging.config 4 | from logging import DEBUG 5 | from logging import FileHandler 6 | from logging import Formatter 7 | from logging import LogRecord 8 | from typing import Any 9 | from typing import Dict 10 | from typing import Optional 11 | 12 | 13 | LOGGING_CONFIG: Dict[str, Any] = { 14 | "version": 1, 15 | "formatters": {"simple": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}}, 16 | "handlers": { 17 | "console": { 18 | "class": "logging.StreamHandler", 19 | "formatter": "simple", 20 | "level": logging.INFO, 21 | } 22 | }, 23 | "loggers": { 24 | "my_logger": { 25 | "handlers": ["console"], 26 | "level": logging.INFO, 27 | "propagate": False, 28 | } 29 | }, 30 | "root": {"handlers": ["console"], "level": logging.WARNING}, 31 | "disable_existing_loggers": False, 32 | } 33 | 34 | 35 | class CustomFormatter(Formatter): 36 | """Logging colored formatter, adapted from https://stackoverflow.com/a/56944256/3638629.""" 37 | 38 | grey = "\x1b[38;21m" 39 | blue = "\x1b[38;5;39m" 40 | yellow = "\x1b[38;5;226m" 41 | red = "\x1b[38;5;196m" 42 | bold_red = "\x1b[31;1m" 43 | reset = "\x1b[0m" 44 | 45 | def __init__(self, fmt: str) -> None: 46 | """Custom formatter for console output.""" 47 | super().__init__() 48 | self.fmt = fmt 49 | self.FORMATS = { 50 | logging.DEBUG: self.grey + self.fmt + self.reset, 51 | logging.INFO: self.blue + self.fmt + self.reset, 52 | logging.WARNING: self.yellow + self.fmt + self.reset, 53 | logging.ERROR: self.red + self.fmt + self.reset, 54 | logging.CRITICAL: self.bold_red + self.fmt + self.reset, 55 | } 56 | 57 | def format(self, record: LogRecord) -> str: 58 | """Used to format a log record object.""" 59 | log_fmt = self.FORMATS.get(record.levelno) 60 | formatter = logging.Formatter(log_fmt) 61 | return formatter.format(record) 62 | 63 | 64 | class DebugFileHandler(FileHandler): 65 | """DebugFileHander.""" 66 | 67 | def __init__( 68 | self, 69 | filename: str, 70 | mode: str = "a", 71 | encoding: Optional[str] = None, 72 | delay: bool = False, 73 | ) -> None: 74 | """Used to debug logging. 75 | 76 | Args: 77 | filename (str): The filename to log to. 78 | mode (str): The mode to open the file. Defaults to "a". 79 | encoding (str, optional): The encoding to use. Defaults to None. 80 | delay (bool): Delay writing to log file. Defaults to False. 81 | """ 82 | super().__init__(filename, mode, encoding, delay) 83 | 84 | def emit(self, record: LogRecord) -> None: 85 | """Used to write a record to a file.""" 86 | if not record.levelno == DEBUG: 87 | return 88 | super().emit(record) 89 | 90 | 91 | class LoggingBase(type): 92 | """Logging metaclass.""" 93 | 94 | def __init__(cls, *args: str) -> None: 95 | """Logging base metaclass.""" 96 | super().__init__(*args) 97 | logging.config.dictConfig(config=LOGGING_CONFIG) 98 | # Explicit name mangling 99 | logger_attribute_name = "_" + cls.__name__ + "__logger" 100 | 101 | # Logger name derived accounting for inheritance for the bonus marks 102 | logger_name = ".".join([c.__name__ for c in cls.mro()[-2::-1]]) 103 | 104 | setattr(cls, logger_attribute_name, logging.getLogger(logger_name)) 105 | -------------------------------------------------------------------------------- /src/czds/base.py: -------------------------------------------------------------------------------- 1 | """czds.base. 2 | 3 | This Base class inherits from our LoggingBase metaclass and gives us 4 | shared logging across any class inheriting from Base. 5 | """ 6 | 7 | import inspect 8 | import os 9 | from typing import Any 10 | from typing import AnyStr 11 | from typing import List 12 | 13 | from .logger import LoggingBase 14 | 15 | 16 | class Base(metaclass=LoggingBase): 17 | """Base class to all other classes within this project.""" 18 | 19 | BASE_URL: AnyStr = "https://czds-api.icann.org" 20 | AUTH_URL: AnyStr = "https://account-api.icann.org/api/authenticate" 21 | BASE_HEADERS: dict = {"Content-Type": "application/json", "Accept": "application/json"} 22 | THREAD_COUNT: int = os.cpu_count() * 5 23 | USERNAME: AnyStr = None 24 | PASSWORD: AnyStr = None 25 | SAVE_PATH: AnyStr = None 26 | OUTPUT_FORMAT: AnyStr = None 27 | 28 | # We create a Connector object and set it to this class property in czds.py 29 | connection = None 30 | 31 | def _chunk(self, items: List[AnyStr], chunk_size: int) -> List[List[AnyStr]]: 32 | chunk_size = max(1, chunk_size) 33 | return (items[i : i + chunk_size] for i in range(0, len(items), chunk_size)) 34 | 35 | def run_threaded(self, method: Any, list_data: List) -> List[str]: 36 | """This method accepts a method and a list of data to run multi-threaded. 37 | 38 | The provided list_data will be chunked into equal part lists per thread. 39 | 40 | TODO: Add ability to configure number of threads by environmental variable and 41 | via input paramater. 42 | 43 | The current configuration for how many threads are used is based on the following 44 | `os.cpu_count() * 5` 45 | 46 | We use the value from the above multiplication by calculating the value to chunk 47 | the list data by. 48 | 49 | This means, that we take the total count of items in the provided list and divide 50 | it by the value from `os.cpu_count() * 5` which equates to 60 on my system. 51 | 52 | For example, using a MacBook Pro 2018 15-inch with 2.6 GHz 6-Core Intel Core i7 and 53 | 16 GB 2400 MHz DDR4 results in 60 threads being created. 54 | 55 | Args: 56 | method (Any): The method to pass each chunk. This method should accept a List (but a smaller list). 57 | list_data (List): The list data to chunk. Each chunk is passed to the provided method in it's own thread. 58 | 59 | Returns: 60 | List[str]: A list of results. 61 | """ 62 | from concurrent.futures import ThreadPoolExecutor 63 | from concurrent.futures import as_completed 64 | 65 | threads = [] 66 | return_list = [] 67 | self.__logger.info(f"Chunking data and running {self.THREAD_COUNT} threads.") 68 | zone_chunks = self._chunk(list_data, int(len(list_data) / self.THREAD_COUNT)) 69 | with ThreadPoolExecutor(max_workers=self.THREAD_COUNT) as executor: 70 | for chunk in zone_chunks: 71 | threads.append(executor.submit(method, chunk)) 72 | count = 0 73 | for task in as_completed(threads): 74 | return_list.append(task.result()) 75 | count += 1 76 | self.__logger.info(f"Retrieved results from {count} threads.") 77 | return return_list 78 | 79 | def log(self, message: AnyStr, level: AnyStr = "info") -> None: 80 | """Used to centralize logging across components. 81 | 82 | We identify the source of the logging class by inspecting the calling stack. 83 | 84 | Args: 85 | message (AnyStr): The log value string to output. 86 | level (AnyStr): The log level. Defaults to "info". 87 | """ 88 | component = None 89 | parent = inspect.stack()[1][0].f_locals.get("self", None) 90 | component = parent.__class__.__name__ 91 | try: 92 | getattr(getattr(parent, f"_{component}__logger"), level)(message) 93 | except AttributeError as ae: 94 | getattr(self.__logger, level)(message + ae) 95 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.5](https://github.com/MSAdministrator/czds/compare/v0.1.4...v0.1.5) (2025-06-24) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Updating internal structure ([#72](https://github.com/MSAdministrator/czds/issues/72)) ([b982901](https://github.com/MSAdministrator/czds/commit/b982901a8620d893fa06f557f557e499d8a9d496)) 9 | 10 | ## [0.1.4](https://github.com/MSAdministrator/czds/compare/v0.1.3...v0.1.4) (2025-06-24) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * bumping preminor version ([#70](https://github.com/MSAdministrator/czds/issues/70)) ([be7378c](https://github.com/MSAdministrator/czds/commit/be7378c6a63c3379f14fdfc25916e788940840cb)) 16 | 17 | ## [0.1.3](https://github.com/MSAdministrator/czds/compare/0.1.2...v0.1.3) (2025-06-23) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * Bumping poetry version in CI ([2c0f77e](https://github.com/MSAdministrator/czds/commit/2c0f77e988c57f138fc0da81280e9ae30202ee9b)) 23 | * more ([bf665ea](https://github.com/MSAdministrator/czds/commit/bf665eaf82052cba9e6e5c9b10ca86fadc6e3465)) 24 | * Moving to pip from pipx ([50a070d](https://github.com/MSAdministrator/czds/commit/50a070dc8cff8269679678e80cc27384ece22bda)) 25 | * Updating actions ([6468a7a](https://github.com/MSAdministrator/czds/commit/6468a7a5353c2b7cd867cd3b9d03ac4b2a10ff47)) 26 | * Updating actions ([da16d39](https://github.com/MSAdministrator/czds/commit/da16d395a5c9225490af69add94b8837258f2698)) 27 | * Updating caching ([69d4d7b](https://github.com/MSAdministrator/czds/commit/69d4d7b2cba990463e3688323f407c187d95b93d)) 28 | * Updating ci ([51d5720](https://github.com/MSAdministrator/czds/commit/51d57208efdd4d171623aa5b5fcc4ff7e9125488)) 29 | * Updating dependencies ([3fcb468](https://github.com/MSAdministrator/czds/commit/3fcb4684f5ba1ce3984a82209bb5155643ab5b7c)) 30 | * Updating deps ([c441a3f](https://github.com/MSAdministrator/czds/commit/c441a3f6883ce32bbd8aed23716e3975679c18e4)) 31 | * Updating from pre-commit hooks ([c26716b](https://github.com/MSAdministrator/czds/commit/c26716b9c65728f9cc9e6e03d6194f5c88487e9c)) 32 | * updating nox-poetry install ([a8cf5e7](https://github.com/MSAdministrator/czds/commit/a8cf5e77e2395ec2960f0a9ae8a5d1815bc59a96)) 33 | * Updating pip ([ad0772c](https://github.com/MSAdministrator/czds/commit/ad0772c02a19084f0d0d9d536849e98e4621fe63)) 34 | * Updating pip ([d78add2](https://github.com/MSAdministrator/czds/commit/d78add270111b650d6b35cd4a8d58a03c441b4c7)) 35 | * Updating tests ([fec3637](https://github.com/MSAdministrator/czds/commit/fec3637133ce7dc25f6815d29b78dc28a42146ec)) 36 | * Updating virtualenv ([f11a79b](https://github.com/MSAdministrator/czds/commit/f11a79ba1ec01550ff017dd545ed9b4fc19db4aa)) 37 | 38 | ## [0.1.2](https://github.com/MSAdministrator/czds/compare/0.1.1...0.1.2) (2023-02-28) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * Fixes [#32](https://github.com/MSAdministrator/czds/issues/32) which will output the correct response when wanting to output a single zone file ([7320ac4](https://github.com/MSAdministrator/czds/commit/7320ac4b4151c192d6bf30217406443576628a57)) 44 | 45 | ## [0.1.1](https://github.com/MSAdministrator/czds/compare/0.1.0...0.1.1) (2023-02-10) 46 | 47 | 48 | ### Documentation 49 | 50 | * Updating README and license.md ([9312569](https://github.com/MSAdministrator/czds/commit/9312569c58102ee035d9dde9a0d095d631f479a1)) 51 | 52 | ## 0.1.0 (2023-02-10) 53 | 54 | 55 | ### ⚠ BREAKING CHANGES 56 | 57 | * This package now requires you to be using Python 3.8 or greater. 58 | * Added the ability to download CZDS zone files using mult-threading. 59 | 60 | ### Features 61 | 62 | * Added output format types ([a19bd8d](https://github.com/MSAdministrator/czds/commit/a19bd8dccf2437bb416f3395b71b98ac29e41d8f)) 63 | * Added the ability to download CZDS zone files using mult-threading. ([84a1530](https://github.com/MSAdministrator/czds/commit/84a1530ff342370a0207eebfc164e5c811864194)) 64 | * Bumped to require python 3.8 or greater. ([85c5a97](https://github.com/MSAdministrator/czds/commit/85c5a97bed021d37e35a593923a6a01b37c9dfcf)) 65 | * Few changes ([954f010](https://github.com/MSAdministrator/czds/commit/954f01024f9e1ee536c87facad5cc947551f0644)) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * Adding ignore quote string in string ([1d35cac](https://github.com/MSAdministrator/czds/commit/1d35cac7365b364187c11c6da231c300c9b60439)) 71 | * Initial code push ([777b3d0](https://github.com/MSAdministrator/czds/commit/777b3d0d0e48325d09dd37d18f3069eef1547a96)) 72 | * More formatting updates ([203cb01](https://github.com/MSAdministrator/czds/commit/203cb012b6f45686c3c1d54992002d420be64cd5)) 73 | * Updated based on pre-commit ([d954b58](https://github.com/MSAdministrator/czds/commit/d954b58b62a0c410618486636e0f750a7afa4aaa)) 74 | * Updating depdencies ([5ccb54d](https://github.com/MSAdministrator/czds/commit/5ccb54df33b67640ce912d00eaed0be7a34fc43a)) 75 | * Updating formatting ([3cc9e0f](https://github.com/MSAdministrator/czds/commit/3cc9e0f81896277c116a6853f3afa850e84569ca)) 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CZDS 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/czds.svg)][pypi status] 4 | [![Status](https://img.shields.io/pypi/status/czds.svg)][pypi status] 5 | [![Python Version](https://img.shields.io/pypi/pyversions/czds)][pypi status] 6 | [![License](https://img.shields.io/pypi/l/czds)][license] 7 | 8 | [![Read the documentation at https://czds.readthedocs.io/](https://img.shields.io/readthedocs/czds/latest.svg?label=Read%20the%20Docs)][read the docs] 9 | [![Code Quality & Tests](https://github.com/MSAdministrator/czds/actions/workflows/tests.yml/badge.svg)](https://github.com/MSAdministrator/czds/actions/workflows/tests.yml) 10 | 11 | [![Codecov](https://codecov.io/gh/MSAdministrator/czds/branch/main/graph/badge.svg)][codecov] 12 | 13 | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit] 14 | [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black] 15 | 16 | [pypi status]: https://pypi.org/project/czds/ 17 | [read the docs]: https://czds.readthedocs.io/ 18 | [tests]: https://github.com/MSAdministrator/czds/actions?workflow=Tests 19 | [codecov]: https://app.codecov.io/gh/MSAdministrator/czds 20 | [pre-commit]: https://github.com/pre-commit/pre-commit 21 | [black]: https://github.com/psf/black 22 | 23 | ## What is CZDS? 24 | 25 | Each Top Level Domain (TLD) is maintained by a registry operator, who also manages a publicly available list of Second Level Domains (SLDs) and the details needed to resolve those domain names to Internet Protocol (IP) addresses. 26 | 27 | The registry operator’s zone data contains the mapping of domain names, associated name server names, and IP addresses for those name servers. These details are updated by the registry operator for its respective TLDs whenever information changes or a domain name is added or removed. 28 | 29 | Each registry operator keeps its zone data in a text file called the Zone File which is updated once every 24 hours. 30 | 31 | ## Features 32 | 33 | - Retrieve Centralized Zone Transfer Files from root DNS servers hosted by ICAAN and other agencies 34 | - Download one or all of the zone files and return data in multiple formats; text, json or a file (default) 35 | - You can now retrieve zone files using multi-threading 36 | 37 | ## Roadmap 38 | 39 | The following are some of the features I am planning on adding but would love to hear everyones thoughts as well. 40 | 41 | - Add ability to search based on domain and/or TLD 42 | - This may include using algorithms like Levenshtein distance, confusables/idna characters, etc. 43 | - Add ability to derive differences between zone files over time 44 | - Add ability to retrieve other contextual external information like WHOIS 45 | - Add ability to save/store data into a database 46 | 47 | ## Requirements 48 | 49 | - You need a CZDS account with ICAAN. You can sign-up [here](https://czds.icann.org) 50 | - Internet access 51 | 52 | ## Installation 53 | 54 | You can install _CZDS_ via [pip] from [PyPI]: 55 | 56 | ```console 57 | $ pip install czds 58 | ``` 59 | 60 | If you are using `poetry` (recommended) you can add it to your package using 61 | 62 | ```console 63 | poetry add czds 64 | ``` 65 | 66 | 67 | ## Usage 68 | 69 | Below is the command line reference but you can also use the current version of czds to retrieve the help by typing ```czds --help```. 70 | 71 | ```console 72 | NAME 73 | czds - Main class for ICAAN CZDS. 74 | 75 | SYNOPSIS 76 | czds GROUP | VALUE | --username=USERNAME --password=PASSWORD --save_directory=SAVE_DIRECTORY 77 | 78 | DESCRIPTION 79 | Main class for ICAAN CZDS. 80 | 81 | ARGUMENTS 82 | USERNAME 83 | Type: ~AnyStr 84 | PASSWORD 85 | Type: ~AnyStr 86 | SAVE_DIRECTORY 87 | Type: ~AnyStr 88 | 89 | GROUPS 90 | GROUP is one of the following: 91 | 92 | BASE_HEADERS 93 | 94 | links 95 | 96 | VALUES 97 | VALUE is one of the following: 98 | 99 | AUTH_URL 100 | 101 | BASE_URL 102 | 103 | OUTPUT_FORMAT 104 | 105 | PASSWORD 106 | 107 | SAVE_PATH 108 | 109 | THREAD_COUNT 110 | 111 | USERNAME 112 | 113 | connection 114 | ``` 115 | 116 | ## Contributing 117 | 118 | Contributions are very welcome. 119 | To learn more, see the [Contributor Guide](CONTRIBUTING.md). 120 | 121 | ## Developmemt 122 | 123 | You can clone the repositry and begin development using 124 | 125 | ```bash 126 | git clone https://github.com/MSAdministrator/czds.git 127 | cd czds 128 | poetry install 129 | ``` 130 | 131 | If you are using `pyenv` to manage your enviroments you can set a config option in poetry to use the set pyenv version of python by running this: 132 | 133 | ```bash 134 | poetry config virtualenvs.prefer-active-python true 135 | poetry install 136 | ``` 137 | ## License 138 | 139 | Distributed under the terms of the [MIT license][LICENSE.md], 140 | _CZDS_ is free and open source software. 141 | 142 | ## Security 143 | 144 | Security concerns are a top priority for us, please review our [Security Policy](SECURITY.md). 145 | 146 | ## Issues 147 | 148 | If you encounter any problems, 149 | please [file an issue](https://github.com/MSAdministrator/czds/issues/new) along with a detailed description. 150 | 151 | ## Credits 152 | 153 | This project was generated from [@MSAdministrator]'s [Hypermodern Python Cookiecutter] template. 154 | 155 | [@MSAdministrator]: https://github.com/MSAdministrator 156 | [pypi]: https://pypi.org/ 157 | [hypermodern python cookiecutter]: https://github.com/cjolowicz/cookiecutter-hypermodern-python 158 | [file an issue]: https://github.com/MSAdministrator/czds/issues 159 | [pip]: https://pip.pypa.io/ 160 | 161 | 162 | 163 | [license]: https://github.com/MSAdministrator/czds/blob/main/LICENSE 164 | [contributor guide]: https://github.com/MSAdministrator/czds/blob/main/CONTRIBUTING.md 165 | [command-line reference]: https://czds.readthedocs.io/en/latest/usage.html 166 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [rickardja@live.com](mailto:rickardja@live.com). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [mozilla coc]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Code Quality & Tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | code-quality: 9 | name: Code Quality Check 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - { python: "3.10", os: "ubuntu-latest", session: "pre-commit" } 16 | env: 17 | NOXSESSION: ${{ matrix.session }} 18 | FORCE_COLOR: "1" 19 | PRE_COMMIT_COLOR: "always" 20 | steps: 21 | - name: Check out the repository 22 | uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python }} 27 | - name: Upgrade pip 28 | run: | 29 | pip install --constraint=.github/workflows/constraints.txt pip 30 | pip --version 31 | - name: Upgrade pip in virtual environments 32 | shell: python 33 | run: | 34 | import os 35 | import pip 36 | 37 | with open(os.environ["GITHUB_ENV"], mode="a") as io: 38 | print(f"VIRTUALENV_PIP={pip.__version__}", file=io) 39 | - name: Install Poetry 40 | run: | 41 | pip install --constraint=.github/workflows/constraints.txt poetry 42 | poetry --version 43 | - name: Install Nox 44 | run: | 45 | pip install --constraint=.github/workflows/constraints.txt nox nox-poetry 46 | nox --version 47 | - name: Compute pre-commit cache key 48 | if: matrix.session == 'pre-commit' 49 | id: pre-commit-cache 50 | shell: python 51 | run: | 52 | import hashlib 53 | import sys 54 | 55 | python = "py{}.{}".format(*sys.version_info[:2]) 56 | payload = sys.version.encode() + sys.executable.encode() 57 | digest = hashlib.sha256(payload).hexdigest() 58 | result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest[:8]) 59 | 60 | print("::set-output name=result::{}".format(result)) 61 | - name: Restore pre-commit cache 62 | uses: actions/cache@v4 63 | if: matrix.session == 'pre-commit' 64 | with: 65 | path: ~/.cache/pre-commit 66 | key: ${{ steps.pre-commit-cache.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }} 67 | restore-keys: | 68 | ${{ steps.pre-commit-cache.outputs.result }}- 69 | 70 | - name: Run Nox 71 | run: | 72 | nox --python=${{ matrix.python }} 73 | tests: 74 | needs: code-quality 75 | name: ${{ matrix.session }} / ${{ matrix.python }} / ${{ matrix.os }} 76 | runs-on: ${{ matrix.os }} 77 | strategy: 78 | fail-fast: false 79 | matrix: 80 | include: 81 | - { python: "3.10", os: "ubuntu-latest", session: "tests" } 82 | - { python: "3.10", os: "macos-latest", session: "tests" } 83 | - { python: "3.10", os: "ubuntu-latest", session: "xdoctest" } 84 | - { python: "3.10", os: "ubuntu-latest", session: "docs-build" } 85 | 86 | env: 87 | NOXSESSION: ${{ matrix.session }} 88 | FORCE_COLOR: "1" 89 | PRE_COMMIT_COLOR: "always" 90 | 91 | steps: 92 | - name: Check out the repository 93 | uses: actions/checkout@v4 94 | 95 | - name: Set up Python ${{ matrix.python }} 96 | uses: actions/setup-python@v5 97 | with: 98 | python-version: ${{ matrix.python }} 99 | 100 | - name: Upgrade pip 101 | run: | 102 | pip install --constraint=.github/workflows/constraints.txt pip 103 | pip --version 104 | 105 | - name: Upgrade pip in virtual environments 106 | shell: python 107 | run: | 108 | import os 109 | import pip 110 | 111 | with open(os.environ["GITHUB_ENV"], mode="a") as io: 112 | print(f"VIRTUALENV_PIP={pip.__version__}", file=io) 113 | 114 | - name: Install Poetry 115 | run: | 116 | pip install --constraint=.github/workflows/constraints.txt poetry 117 | poetry --version 118 | 119 | - name: Install Nox 120 | run: | 121 | pip install --constraint=.github/workflows/constraints.txt nox nox-poetry 122 | nox --version 123 | 124 | - name: Run Nox 125 | run: | 126 | nox --python=${{ matrix.python }} 127 | 128 | - name: Upload documentation 129 | if: matrix.session == 'docs-build' 130 | uses: actions/upload-artifact@v4 131 | with: 132 | name: docs 133 | path: docs/_build 134 | 135 | # coverage: 136 | # runs-on: ubuntu-latest 137 | # needs: tests 138 | # steps: 139 | # - name: Check out the repository 140 | # uses: actions/checkout@v3 141 | 142 | # - name: Set up Python 143 | # uses: actions/setup-python@v4 144 | # with: 145 | # python-version: "3.10" 146 | 147 | # - name: Upgrade pip 148 | # run: | 149 | # pip install --constraint=.github/workflows/constraints.txt pip 150 | # pip --version 151 | 152 | # - name: Install Poetry 153 | # run: | 154 | # pipx install --pip-args=--constraint=.github/workflows/constraints.txt poetry 155 | # poetry --version 156 | 157 | # - name: Install Nox 158 | # run: | 159 | # pipx install --pip-args=--constraint=.github/workflows/constraints.txt nox 160 | # pipx inject --pip-args=--constraint=.github/workflows/constraints.txt nox nox-poetry 161 | # nox --version 162 | 163 | # - name: Download coverage data 164 | # uses: actions/download-artifact@v3 165 | # with: 166 | # name: coverage-data 167 | 168 | # - name: Combine coverage data and display human readable report 169 | # run: | 170 | # nox --session=coverage 171 | 172 | # - name: Create coverage report 173 | # run: | 174 | # nox --session=coverage -- xml 175 | 176 | # - name: Upload coverage report 177 | # uses: codecov/codecov-action@v3.1.1 178 | -------------------------------------------------------------------------------- /src/czds/configuration.py: -------------------------------------------------------------------------------- 1 | """Configuration file.""" 2 | 3 | import json 4 | import os 5 | from string import Template 6 | from typing import AnyStr 7 | from typing import Dict 8 | 9 | import yaml 10 | from attrs import asdict 11 | from attrs import define 12 | from attrs import field 13 | from attrs import fields 14 | from prompt_toolkit import PromptSession 15 | 16 | from .base import Base 17 | 18 | 19 | @define 20 | class Authorization: 21 | """Contains settings for authorization to Graph API and Azure endpoints.""" 22 | 23 | client_id: AnyStr = field(factory=str, metadata={"question": Template("Please provide your Graph API Client ID: ")}) 24 | client_secret: AnyStr = field( 25 | factory=str, metadata={"question": Template("Please provide your Graph API Client Secret: ")} 26 | ) 27 | tenant_id: AnyStr = field(factory=str, metadata={"question": Template("Please provide your Graph API Tenant ID: ")}) 28 | subscription_id: AnyStr = field( 29 | factory=str, metadata={"question": Template("Please provide your Azure Sentinel Subscription ID: ")} 30 | ) 31 | resource_group_name: AnyStr = field( 32 | factory=str, metadata={"question": Template("Please provide your Azure Sentinel Resource Group Name: ")} 33 | ) 34 | workspace_name: AnyStr = field( 35 | factory=str, metadata={"question": Template("Please provide your Azure Sentinel Workspace Name: ")} 36 | ) 37 | api_version: AnyStr = "2022-11-01" 38 | 39 | 40 | @define 41 | class Configuration: 42 | """The main configuration data model.""" 43 | 44 | authorization: Authorization = field(factory=Authorization) 45 | 46 | def __attrs_post_init__(self): 47 | """Casting data objects to their correct type during initialization.""" 48 | if self.authorization: 49 | self.authorization = Authorization(**self.authorization) 50 | 51 | 52 | class ConfigurationManager(Base): 53 | """The main class used to manage retreiving and saving configuration files from disk.""" 54 | 55 | CONFIG_PATH = "~/.config/czds.yml" 56 | session = PromptSession() 57 | 58 | def __init__(self) -> None: 59 | """Determines if configuration file exists or not.""" 60 | self.config_path = self._get_absolute_path(path=self.CONFIG_PATH) 61 | if not os.path.exists(self.config_path): 62 | self._save_to_disk(path=self.config_path, data=self._prompt()) 63 | 64 | def _prompt(self) -> Dict[str, str]: 65 | """Prompts the user to enter values for the defined services in our Configuration data model. 66 | 67 | The questions are metadata within our Configuration data model using string Templates. 68 | 69 | Returns: 70 | Dict[str, str]: A dictionary of the provided answers to our configuration questions. 71 | """ 72 | authorization = { 73 | "tenant_id": self.session.prompt(fields(Authorization).tenant_id.metadata["question"].substitute()), 74 | "client_id": self.session.prompt(fields(Authorization).client_id.metadata["question"].substitute()), 75 | "client_secret": self.session.prompt(fields(Authorization).client_secret.metadata["question"].substitute()), 76 | "subscription_id": self.session.prompt( 77 | fields(Authorization).subscription_id.metadata["question"].substitute() 78 | ), 79 | "resource_group_name": self.session.prompt( 80 | fields(Authorization).resource_group_name.metadata["question"].substitute() 81 | ), 82 | "workspace_name": self.session.prompt( 83 | fields(Authorization).workspace_name.metadata["question"].substitute() 84 | ), 85 | } 86 | return asdict(Configuration(authorization=authorization)) 87 | 88 | def _read_from_disk(self, path: str) -> Dict[str, str]: 89 | """Retreives the configuration file values from a given path on the disk. 90 | 91 | Args: 92 | path (str): The path to the configuration file on disk. 93 | 94 | Raises: 95 | FileNotFoundError: Raises when the file cannot be found. 96 | IsADirectoryError: Raises when the provided path is a directory. 97 | 98 | Returns: 99 | Dict[str, str]: A dictionary containing the defined services keys and values. 100 | """ 101 | if os.path.exists(path) and os.path.isfile(path): 102 | try: 103 | with open(path) as f: 104 | if path.endswith(".json"): 105 | return json.load(f) 106 | elif path.endswith(".yml") or path.endswith(".yaml"): 107 | return yaml.load(f, Loader=yaml.SafeLoader) 108 | else: 109 | raise FileNotFoundError(f"The provided path value '{path}' is not one of '.yml'.") 110 | except Exception as e: 111 | self.__logger.warning(f"The provided config file {path} is not in the correct format. {e}") 112 | elif os.path.isdir(path): 113 | raise IsADirectoryError(f"The provided path is a directory and must be a file: {path}") 114 | 115 | def _save_to_disk(self, path: str, data: Dict[str, str]) -> None: 116 | """Saves the provided configuration data to the provided path. 117 | 118 | Args: 119 | path (str): The path to save our configuration data. 120 | data (Dict[str, str]): The data to save within our configuration file. 121 | 122 | Raises: 123 | Exception: Raises when an unknown exception is made. 124 | FileNotFoundError: Raises when the file cannot be found. 125 | IsADirectoryError: Raises when the provided value is a directory. 126 | """ 127 | try: 128 | if not os.path.exists(os.path.dirname(path)): 129 | try: 130 | os.makedirs(os.path.dirname(path)) 131 | except Exception as e: 132 | message = f"Attempted to create the provided directories '{path}' but was unable to." 133 | self.__logger.critical(message + e) 134 | raise e 135 | with open(path, "w+") as f: 136 | if path.endswith(".json"): 137 | json.dump(data, f) 138 | elif path.endswith(".yml") or path.endswith(".yaml"): 139 | yaml.dump(data, f) 140 | else: 141 | raise FileNotFoundError(f"The provided path value '{path}' is not one of '.yml'.") 142 | except IsADirectoryError as ie: 143 | raise ie 144 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Nox sessions.""" 2 | 3 | import os 4 | import shlex 5 | import shutil 6 | import sys 7 | from pathlib import Path 8 | from textwrap import dedent 9 | 10 | import nox 11 | 12 | 13 | try: 14 | from nox_poetry import Session 15 | from nox_poetry import session 16 | except ImportError: 17 | message = f"""\ 18 | Nox failed to import the 'nox-poetry' package. 19 | 20 | Please install it using the following command: 21 | 22 | {sys.executable} -m pip install nox-poetry""" 23 | raise SystemExit(dedent(message)) from None 24 | 25 | 26 | package = "czds" 27 | python_versions = ["3.10", "3.9", "3.8", "3.7"] 28 | nox.needs_version = ">= 2021.6.6" 29 | nox.options.sessions = ( 30 | "pre-commit", 31 | "safety", 32 | "mypy", 33 | "tests", 34 | "typeguard", 35 | "xdoctest", 36 | "docs-build", 37 | ) 38 | 39 | 40 | def activate_virtualenv_in_precommit_hooks(session: Session) -> None: 41 | """Activate virtualenv in hooks installed by pre-commit. 42 | 43 | This function patches git hooks installed by pre-commit to activate the 44 | session's virtual environment. This allows pre-commit to locate hooks in 45 | that environment when invoked from git. 46 | 47 | Args: 48 | session: The Session object. 49 | """ 50 | assert session.bin is not None # noqa: S101 51 | 52 | # Only patch hooks containing a reference to this session's bindir. Support 53 | # quoting rules for Python and bash, but strip the outermost quotes so we 54 | # can detect paths within the bindir, like /python. 55 | bindirs = [ 56 | bindir[1:-1] if bindir[0] in "'\"" else bindir for bindir in (repr(session.bin), shlex.quote(session.bin)) 57 | ] 58 | 59 | virtualenv = session.env.get("VIRTUAL_ENV") 60 | if virtualenv is None: 61 | return 62 | 63 | headers = { 64 | # pre-commit < 2.16.0 65 | "python": f"""\ 66 | import os 67 | os.environ["VIRTUAL_ENV"] = {virtualenv!r} 68 | os.environ["PATH"] = os.pathsep.join(( 69 | {session.bin!r}, 70 | os.environ.get("PATH", ""), 71 | )) 72 | """, 73 | # pre-commit >= 2.16.0 74 | "bash": f"""\ 75 | VIRTUAL_ENV={shlex.quote(virtualenv)} 76 | PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" 77 | """, 78 | # pre-commit >= 2.17.0 on Windows forces sh shebang 79 | "/bin/sh": f"""\ 80 | VIRTUAL_ENV={shlex.quote(virtualenv)} 81 | PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" 82 | """, 83 | } 84 | 85 | hookdir = Path(".git") / "hooks" 86 | if not hookdir.is_dir(): 87 | return 88 | 89 | for hook in hookdir.iterdir(): 90 | if hook.name.endswith(".sample") or not hook.is_file(): 91 | continue 92 | 93 | if not hook.read_bytes().startswith(b"#!"): 94 | continue 95 | 96 | text = hook.read_text() 97 | 98 | if not any(Path("A") == Path("a") and bindir.lower() in text.lower() or bindir in text for bindir in bindirs): 99 | continue 100 | 101 | lines = text.splitlines() 102 | 103 | for executable, header in headers.items(): 104 | if executable in lines[0].lower(): 105 | lines.insert(1, dedent(header)) 106 | hook.write_text("\n".join(lines)) 107 | break 108 | 109 | 110 | @session(name="pre-commit", python=python_versions[0]) 111 | def precommit(session: Session) -> None: 112 | """Lint using pre-commit.""" 113 | args = session.posargs or [ 114 | "run", 115 | "--all-files", 116 | "--hook-stage=manual", 117 | "--show-diff-on-failure", 118 | ] 119 | session.install( 120 | "black", 121 | "darglint", 122 | "flake8", 123 | "flake8-bandit", 124 | "flake8-bugbear", 125 | "flake8-docstrings", 126 | "flake8-rst-docstrings", 127 | "isort", 128 | "pep8-naming", 129 | "pre-commit", 130 | "pre-commit-hooks", 131 | "pyupgrade", 132 | ) 133 | session.run("pre-commit", *args) 134 | if args and args[0] == "install": 135 | activate_virtualenv_in_precommit_hooks(session) 136 | 137 | 138 | @session(python=python_versions[0]) 139 | def safety(session: Session) -> None: 140 | """Scan dependencies for insecure packages.""" 141 | requirements = session.poetry.export_requirements() 142 | session.install("safety") 143 | session.run("safety", "check", "--full-report", f"--file={requirements}") 144 | 145 | 146 | @session(python=python_versions) 147 | def mypy(session: Session) -> None: 148 | """Type-check using mypy.""" 149 | args = session.posargs or ["src", "tests", "docs/conf.py"] 150 | session.install(".") 151 | session.install("mypy", "pytest") 152 | session.run("mypy", *args) 153 | if not session.posargs: 154 | session.run("mypy", f"--python-executable={sys.executable}", "noxfile.py") 155 | 156 | 157 | @session(python=python_versions) 158 | def tests(session: Session) -> None: 159 | """Run the test suite.""" 160 | session.install(".") 161 | session.install("coverage[toml]", "pytest", "pygments") 162 | try: 163 | session.run("coverage", "run", "--parallel", "-m", "pytest", *session.posargs) 164 | finally: 165 | if session.interactive: 166 | session.notify("coverage", posargs=[]) 167 | 168 | 169 | @session(python=python_versions[0]) 170 | def coverage(session: Session) -> None: 171 | """Produce the coverage report.""" 172 | args = session.posargs or ["report"] 173 | 174 | session.install("coverage[toml]") 175 | 176 | if not session.posargs and any(Path().glob(".coverage.*")): 177 | session.run("coverage", "combine") 178 | 179 | session.run("coverage", *args) 180 | 181 | 182 | @session(python=python_versions[0]) 183 | def typeguard(session: Session) -> None: 184 | """Runtime type checking using Typeguard.""" 185 | session.install(".") 186 | session.install("pytest", "typeguard", "pygments") 187 | session.run("pytest", f"--typeguard-packages={package}", *session.posargs) 188 | 189 | 190 | @session(python=python_versions) 191 | def xdoctest(session: Session) -> None: 192 | """Run examples with xdoctest.""" 193 | if session.posargs: 194 | args = [package, *session.posargs] 195 | else: 196 | args = [f"--modname={package}", "--command=all"] 197 | if "FORCE_COLOR" in os.environ: 198 | args.append("--colored=1") 199 | 200 | session.install(".") 201 | session.install("xdoctest[colors]") 202 | session.run("python", "-m", "xdoctest", *args) 203 | 204 | 205 | @session(name="docs-build", python=python_versions[0]) 206 | def docs_build(session: Session) -> None: 207 | """Build the documentation.""" 208 | args = session.posargs or ["docs", "docs/_build"] 209 | if not session.posargs and "FORCE_COLOR" in os.environ: 210 | args.insert(0, "--color") 211 | 212 | session.install(".") 213 | session.install("sphinx", "sphinx-click", "furo", "myst-parser") 214 | 215 | build_dir = Path("docs", "_build") 216 | if build_dir.exists(): 217 | shutil.rmtree(build_dir) 218 | 219 | session.run("sphinx-build", *args) 220 | 221 | 222 | @session(python=python_versions[0]) 223 | def docs(session: Session) -> None: 224 | """Build and serve the documentation with live reloading on file changes.""" 225 | args = session.posargs or ["--open-browser", "docs", "docs/_build"] 226 | session.install(".") 227 | session.install("sphinx", "sphinx-autobuild", "sphinx-click", "furo", "myst-parser") 228 | 229 | build_dir = Path("docs", "_build") 230 | if build_dir.exists(): 231 | shutil.rmtree(build_dir) 232 | 233 | session.run("sphinx-autobuild", *args) 234 | -------------------------------------------------------------------------------- /src/czds/connector.py: -------------------------------------------------------------------------------- 1 | """Main connector class.""" 2 | 3 | import cgi 4 | import json 5 | import os 6 | import zlib 7 | from typing import Any 8 | from typing import AnyStr 9 | from typing import Dict 10 | from typing import List 11 | 12 | from requests import Response 13 | from requests import Session 14 | from requests.exceptions import HTTPError 15 | 16 | from .base import Base 17 | from .exceptions import CZDSConnectionError 18 | from .exceptions import UnsupportedTypeError 19 | 20 | 21 | class CZDSConnector(Base): 22 | """Main CZDS connection classself. 23 | 24 | All API Calls go through this class. 25 | """ 26 | 27 | def __init__(self) -> None: 28 | """Creates a credential property and retrieves our access token from authenticationself.""" 29 | self.credential: dict = {"username": Base.USERNAME, "password": Base.PASSWORD} 30 | self.token = self.get_token() 31 | 32 | def _parse_line(self, line: AnyStr) -> Dict[str, str]: 33 | """Parses a line from a zone file into a dictionary. 34 | 35 | Args: 36 | line (AnyStr): A line from a zone file. 37 | 38 | Returns: 39 | Dict[str, str]: A dictionary of the line. 40 | """ 41 | count = 1 42 | zone_dict = {} 43 | record_data_list = [] 44 | for item in line.split(): 45 | if count == 1: 46 | zone_dict["dns_record"] = item.rstrip(".") 47 | elif count == 2: 48 | try: 49 | int(item) 50 | zone_dict["ttl"] = item 51 | except Exception as e: 52 | zone_dict["record_class"] = item 53 | elif count == 3: 54 | if zone_dict.get("record_class"): 55 | zone_dict["ttl"] = item 56 | else: 57 | zone_dict["record_class"] = item 58 | elif count == 4: 59 | zone_dict["record_type"] = item 60 | else: 61 | record_data_list.append(item) 62 | count += 1 63 | zone_dict["record_data"] = " ".join(record_data_list) 64 | return zone_dict 65 | 66 | def _request( 67 | self, 68 | url: AnyStr, 69 | method: AnyStr = "GET", 70 | data: Any = None, 71 | headers: Dict[str, str] = None, 72 | stream: bool = False, 73 | ) -> Response: 74 | """Main method to make all HTTP requests. 75 | 76 | Args: 77 | url (AnyStr): The URL to send the request to. 78 | method (AnyStr): The HTTP method to use. Defaults to "GET". 79 | data (Any): The data argument provided to requests. Defaults to None. 80 | headers (Dict[str, str], optional): The headers to use with the request. Defaults to None. 81 | stream (bool): Whether or not to stream response content. Defaults to False. 82 | 83 | Raises: 84 | CZDSConnectionError: Raises when a connection error occurs. 85 | 86 | Returns: 87 | Response: The requests Response object. 88 | """ 89 | response = Session().request(method=method, url=url, headers=headers, data=data, stream=stream) 90 | try: 91 | response.raise_for_status() 92 | except HTTPError as error: 93 | if error.response.status_code == 404: 94 | raise CZDSConnectionError(status_code=error.response.status_code, name="InvalidURL", http_error=error) 95 | elif error.response.status_code == 401: 96 | raise CZDSConnectionError(status_code=401, name="InvalidAuthentication", http_error=error) 97 | elif error.response.status_code == 500: 98 | raise CZDSConnectionError(status_code=500, name="InternalServerError", http_error=error) 99 | else: 100 | raise CZDSConnectionError(status_code=error.response.status_code, name="UnknownError", http_error=error) 101 | return response 102 | 103 | def get_token(self) -> AnyStr: 104 | """Authenticates and retrieves access token for all other API calls. 105 | 106 | Returns: 107 | AnyStr: An access token. 108 | """ 109 | return self._request( 110 | method="POST", url=self.AUTH_URL, data=json.dumps(self.credential), headers=self.BASE_HEADERS 111 | ).json()["accessToken"] 112 | 113 | def _get(self, url: AnyStr) -> Response: 114 | """This method is used to make all calls to CZDS. 115 | 116 | Args: 117 | url (AnyStr): The URL to make the request against. 118 | 119 | Returns: 120 | Response: A requests.Response object. 121 | """ 122 | headers = Base.BASE_HEADERS 123 | headers.update({"Authorization": f"Bearer {self.token}"}) 124 | self.__logger.debug(f"Making request to '{url}'.") 125 | return self._request(url=url, headers=headers, stream=True) 126 | 127 | def _get_zone_links(self) -> List[str]: 128 | """Retrieves all available CZDS zone file links. 129 | 130 | Returns: 131 | List[str]: A list of available CZDS zone file links. 132 | """ 133 | links_url = self.BASE_URL + "/czds/downloads/links" 134 | return self._get(links_url).json() 135 | 136 | def _output_to_disk(self, response: Response, file_path: AnyStr) -> None: 137 | """Writes the response content to disk. 138 | 139 | Args: 140 | response (Response): The response object to write to disk. 141 | file_path (AnyStr): The path to write the response content to. 142 | """ 143 | with open(file_path, "wb") as f: 144 | for chunk in response.raw.stream(1024, decode_content=False): 145 | if chunk: 146 | f.write(chunk) 147 | 148 | def _output_to_text_stream(self, response: Response, file_path: AnyStr) -> List[AnyStr]: 149 | """Writes the response content to stdout. 150 | 151 | Args: 152 | response (Response): The response object to write to stdout. 153 | file_path (AnyStr): The path to write the response content to. 154 | 155 | Returns: 156 | List[AnyStr]: A list of lines from the response content. 157 | """ 158 | return_list: List[AnyStr] = [] 159 | with open(file_path, "wb") as f: 160 | dec = zlib.decompressobj(32 + zlib.MAX_WBITS) 161 | for chunk in response.raw.stream(1024, decode_content=False): 162 | if chunk: 163 | rv = dec.decompress(chunk) 164 | data = rv.decode("utf-8", errors="ignore") 165 | for line in data.split("\n"): 166 | return_list.append(line) 167 | print(line) 168 | return return_list 169 | 170 | def _output_to_json_stream(self, response: Response, file_path: AnyStr) -> List[Dict[str, Any]]: 171 | """Writes the response content to stdout. 172 | 173 | Args: 174 | response (Response): The response object to write to stdout. 175 | file_path (AnyStr): The path to write the response content to. 176 | 177 | Returns: 178 | List[Dict[str, Any]]: A list of lines from the response content in dictionary format. 179 | """ 180 | return_list: List[Dict[str, Any]] = [] 181 | with open(file_path, "wb") as f: 182 | dec = zlib.decompressobj(32 + zlib.MAX_WBITS) 183 | for chunk in response.raw.stream(1024, decode_content=False): 184 | if chunk: 185 | rv = dec.decompress(chunk) 186 | data = rv.decode("utf-8", errors="ignore") 187 | for line in data.split("\n"): 188 | parsed_dict: Dict[str, Any] = self._parse_line(line=line) 189 | return_list.append(parsed_dict) 190 | print(parsed_dict) 191 | return return_list 192 | 193 | def _download_single_zone_file(self, zone_file_link: AnyStr) -> AnyStr: 194 | """Downloas the zone file from the provided URL. 195 | 196 | Args: 197 | zone_file_link (AnyStr): A CZDS Zone file to download from. 198 | 199 | Raises: 200 | UnsupportedTypeError: Raises when the output format is not supported. 201 | 202 | Returns: 203 | AnyStr: The path that the zone file was downloaded to. 204 | """ 205 | response = self._get(zone_file_link) 206 | zone_name = zone_file_link.rsplit("/", 1)[-1].rsplit(".")[-2] 207 | 208 | # Try to get the filename from the header 209 | _, option = cgi.parse_header(response.headers["content-disposition"]) 210 | filename = option["filename"] 211 | 212 | # If could get a filename from the header, then makeup one like [tld].txt.gz 213 | if not filename: 214 | filename = zone_name + ".txt" 215 | path = os.path.join(Base.SAVE_PATH, filename) 216 | if not Base.OUTPUT_FORMAT: 217 | self._output_to_disk(response=response, file_path=path) 218 | elif Base.OUTPUT_FORMAT == "text": 219 | self._output_to_text_stream(response=response, file_path=path) 220 | elif Base.OUTPUT_FORMAT == "json": 221 | self._output_to_json_stream(response=response, file_path=path) 222 | else: 223 | raise UnsupportedTypeError( 224 | f"Unsupported output format '{Base.OUTPUT_FORMAT}'. Supported formats are 'none', 'text', or 'json'." 225 | ) 226 | return path 227 | 228 | def download(self, zone_file_list: AnyStr or List[AnyStr]) -> AnyStr: 229 | """Main method used to download zone files. 230 | 231 | Args: 232 | zone_file_list (AnyStr): One or more zone file(s) to download. 233 | 234 | Raises: 235 | UnsupportedTypeError: Raised when a unsupported type is provided. 236 | 237 | Returns: 238 | AnyStr: The path that the zone file was saved to. 239 | """ 240 | if Base.SAVE_PATH: 241 | if not os.path.exists(Base.SAVE_PATH): 242 | os.makedirs(Base.SAVE_PATH) 243 | if isinstance(zone_file_list, list): 244 | return [self._download_single_zone_file(zone_file_link=link) for link in zone_file_list] 245 | elif isinstance(zone_file_list, str): 246 | return self._download_single_zone_file(zone_file_link=zone_file_list) 247 | else: 248 | self.__logger.critical( 249 | f"Unable to download. Unknown data structure provided to this method. {zone_file_list}" 250 | ) 251 | raise UnsupportedTypeError("""Unknown data type. Should be 'list' or 'str'.""") 252 | --------------------------------------------------------------------------------