diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef220d1..d3d6a53 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -80,7 +80,7 @@ repos: repo: https://github.com/commitizen-tools/commitizen rev: faff6a61f4f53585a965addd06156eedf0027643 # frozen: v3.25.0 - hooks: - - entry: hatch run python3 -m mypy + - entry: hatch run types:check id: mypy language: system name: mypy diff --git a/pyproject.toml b/pyproject.toml index d3bcfe1..c4e9510 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,12 @@ setup-dev = ["setup-pre-commit"] pre-commit = "python3 -m pre_commit run {args:--all}" +[tool.hatch.envs.types] +dependencies = ["mypy>=1.8", "types-lxml"] + +[tool.hatch.envs.types.scripts] +check = "mypy --non-interactive --install-types {args:src/gvm_sync_targets tests}" + [tool.hatch.envs.testall] type = "container" dependencies = ["coverage[toml]>=6.5", "pytest>=6.0"] @@ -170,7 +176,6 @@ extra_checks = true warn_unreachable = true warn_return_any = true warn_no_return = true -incremental = false enable_error_code = [ "redundant-self", "redundant-expr", diff --git a/src/gvm_sync_targets/models/__init__.py b/src/gvm_sync_targets/models/__init__.py index 86e0ea8..42db30b 100644 --- a/src/gvm_sync_targets/models/__init__.py +++ b/src/gvm_sync_targets/models/__init__.py @@ -14,13 +14,23 @@ ) from gvm_sync_targets.models.auth_response import AuthenticateResponse from gvm_sync_targets.models.response import Response +from gvm_sync_targets.models.targets_response import GetTargetsResponse from gvm_sync_targets.util import Element +__all__ = [ + "GetAssetsResponse", + "AuthenticateResponse", + "DeleteAssetResponse", + "CreateAssetResponse", + "GetTargetsResponse", +] + _MODEL_MAP: Mapping[str, type[Response]] = { "get_assets_response": GetAssetsResponse, "authenticate_response": AuthenticateResponse, "delete_asset_response": DeleteAssetResponse, "create_asset_response": CreateAssetResponse, + "get_targets_response": GetTargetsResponse, } diff --git a/src/gvm_sync_targets/models/assets_response.py b/src/gvm_sync_targets/models/assets_response.py index 3222a28..8b0339f 100644 --- a/src/gvm_sync_targets/models/assets_response.py +++ b/src/gvm_sync_targets/models/assets_response.py @@ -3,18 +3,20 @@ # SPDX-License-Identifier: MIT import datetime -from typing import Annotated, Optional +from typing import Optional -from pydantic import PlainSerializer from pydantic_xml import attr, element -from gvm_sync_targets.models.model import Model +from gvm_sync_targets.models.model import IntBoolean, Model +from gvm_sync_targets.models.resource import ( + Count, + Filters, + Pagination, + Resource, + Sort, +) from gvm_sync_targets.models.response import Response -IntBoolean = Annotated[ - bool, PlainSerializer(lambda value: 1 if value else 0, return_type=int) -] - class Severity(Model, tag="severity"): value: Optional[float] = element(default=None) @@ -40,18 +42,6 @@ class Host(Model, tag="host"): details: list[Detail] = element("detail", default_factory=list) -class Owner(Model, tag="owner"): - name: str = element() - - -class Permission(Model, tag="permission"): - name: str = element() - - -class Permissions(Model, tag="permissions"): - permissions: list[Permission] = element() - - class Tag(Model, tag="tag"): uuid: str = attr("id") @@ -86,67 +76,19 @@ class Identifiers(Model, tag="identifiers"): identifiers: list[Identifier] = element() -class Asset(Model, tag="asset"): - uuid: str = attr("id") - - owner: Owner - name: str = element() - comment: Optional[str] = element(default=None) - creation_time: datetime.datetime = element() - modification_time: datetime.datetime = element() - writable: IntBoolean = element() - in_use: IntBoolean = element() - - permissions: Permissions = element() +class Asset(Resource, tag="asset"): user_tags: Optional[UserTags] = element(default=None) identifiers: Identifiers = element() type: str = element() host: Host = element("host") -class Keyword(Model, tag="keyword"): - column: str = element() - relation: str = element() - value: str = element() - - -class Keywords(Model, tag="keywords"): - keywords: list[Keyword] = element() - - -class Filters(Model, tag="filters"): - uuid: str = attr("id") - - term: str = element() - keywords: Keywords = element() - - -class AssetCount(Model, tag="asset_count"): - total: int - filtered: int = element() - page: int = element() - - -class Pagination(Model): - start: int = attr() - max: int = attr() - - -class SortField(Model, tag="field"): - name: str - order: str = element() - - -class Sort(Model, tag="sort"): - field: SortField - - class GetAssetsResponse(Response, tag="get_assets_response"): assets: list[Asset] = [] filters: Filters sort: Sort pagination: Pagination = element("assets") - asset_count: AssetCount + asset_count: Count = element("asset_count") class DeleteAssetResponse(Response, tag="delete_asset_response"): diff --git a/src/gvm_sync_targets/models/model.py b/src/gvm_sync_targets/models/model.py index 1644a6d..bfc859d 100644 --- a/src/gvm_sync_targets/models/model.py +++ b/src/gvm_sync_targets/models/model.py @@ -2,8 +2,15 @@ # # SPDX-License-Identifier: MIT +from typing import Annotated + +from pydantic import PlainSerializer from pydantic_xml import BaseXmlModel +IntBoolean = Annotated[ + bool, PlainSerializer(lambda value: 1 if value else 0, return_type=int) +] + -class Model(BaseXmlModel, extra="forbid"): +class Model(BaseXmlModel, search_mode="unordered", extra="forbid"): pass diff --git a/src/gvm_sync_targets/models/resource.py b/src/gvm_sync_targets/models/resource.py new file mode 100644 index 0000000..e16566a --- /dev/null +++ b/src/gvm_sync_targets/models/resource.py @@ -0,0 +1,69 @@ +import datetime +from typing import Optional + +from pydantic_xml import attr, element + +from gvm_sync_targets.models.model import IntBoolean, Model + + +class Owner(Model, tag="owner"): + name: str = element() + + +class Permission(Model, tag="permission"): + name: str = element() + + +class Permissions(Model, tag="permissions"): + permissions: list[Permission] = element() + + +class Keyword(Model, tag="keyword"): + column: str = element() + relation: str = element() + value: str = element() + + +class Keywords(Model, tag="keywords"): + keywords: list[Keyword] = element() + + +class Filters(Model, tag="filters"): + uuid: str = attr("id") + + term: str = element() + keywords: Keywords = element() + + +class Count(Model): + total: int + filtered: int = element() + page: int = element() + + +class Pagination(Model): + start: int = attr() + max: int = attr() + + +class SortField(Model, tag="field"): + name: str + order: str = element() + + +class Sort(Model, tag="sort"): + field: SortField + + +class Resource(Model): + uuid: str = attr("id") + + owner: Owner + name: str = element() + comment: Optional[str] = element(default=None) + creation_time: datetime.datetime = element() + modification_time: datetime.datetime = element() + writable: IntBoolean = element() + in_use: IntBoolean = element() + + permissions: Permissions = element() diff --git a/src/gvm_sync_targets/models/targets_response.py b/src/gvm_sync_targets/models/targets_response.py new file mode 100644 index 0000000..950f4d7 --- /dev/null +++ b/src/gvm_sync_targets/models/targets_response.py @@ -0,0 +1,72 @@ +from typing import Optional + +from pydantic_xml import attr, element + +from gvm_sync_targets.models.model import IntBoolean, Model +from gvm_sync_targets.models.resource import ( + Count, + Filters, + Pagination, + Resource, + Sort, +) +from gvm_sync_targets.models.response import Response + + +class Credential(Model): + uuid: str = attr("id") + name: Optional[str] = element(default=None) + + trash: IntBoolean = element(tag="trash") + + +class SSHCredential(Credential, tag="ssh_credential"): + port: int = element(tag="port") + + +class SMBCredential(Credential, tag="smb_credential"): + pass + + +class ESXICredential(Credential, tag="esxi_credential"): + pass + + +class SNMPCredential(Credential, tag="snmp_credential"): + pass + + +class SSHElevateCredential(Credential, tag="ssh_elevate_credential"): + pass + + +class PortList(Model, tag="port_list"): + uuid: str = attr("id") + + name: str = element() + trash: IntBoolean = element() + + +class Target(Resource, tag="target"): + hosts: str = element() + exclude_hosts: Optional[str] = element(default=None) + max_hosts: int = element() + port_list: PortList = element() + ssh_credential: SSHCredential = element() + smb_credential: SMBCredential = element() + esxi_credential: ESXICredential = element() + snmp_credential: SNMPCredential = element() + ssh_elevate_credential: SSHElevateCredential = element() + + reverse_lookup_only: IntBoolean = element() + reverse_lookup_unify: IntBoolean = element() + alive_tests: str = element() + allow_simultaneous_ips: IntBoolean = element() + + +class GetTargetsResponse(Response, tag="get_targets_response"): + targets: list[Target] = element() + filters: Filters + sort: Sort + pagination: Pagination = element("targets") + asset_count: Count = element("target_count") diff --git a/src/gvm_sync_targets/py.typed b/src/gvm_sync_targets/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/gvm_sync_targets/models/targets_response_test.py b/tests/gvm_sync_targets/models/targets_response_test.py new file mode 100644 index 0000000..470fe3f --- /dev/null +++ b/tests/gvm_sync_targets/models/targets_response_test.py @@ -0,0 +1,144 @@ +from gvm_sync_targets.models import GetTargetsResponse + +data = """ + + + + admin + + All Hosts + + 2024-05-15T17:37:30Z + 2024-05-15T17:37:30Z + 1 + 0 + + + Everything + + + 1.2.3.4, 1::1 + + 44 + + All IANA assigned TCP and UDP + 0 + + + SSH auth + 22 + 0 + + + + 0 + + + + 0 + + + + 0 + + + + 0 + + 0 + 0 + Scan Config Default + 1 + + + + admin + + New All Hosts + + 2024-05-17T05:59:19Z + 2024-05-17T05:59:19Z + 1 + 0 + + + Everything + + + 1.2.3.4, 1::1 + + 41 + + All IANA assigned TCP and UDP + 0 + + + SSH auth + 22 + 0 + + + + 0 + + 0 + 0 + 0 + 0 + 0 + Scan Config Default1 + + + adminUnnamed + 2024-05-15T20:41:23Z + 2024-05-15T20:41:23Z + 11 + Everything + 1.2.3.4, 1::1 + 41 + All IANA assigned TCP0 + SSH auth220 + 00 + 00 + 00 + Scan Config Default1 + + + first=1 rows=10 sort=name + + + first + = + 1 + + + rows + = + 10 + + + sort + = + name + + + + + + name + ascending + + + + + 3 + 3 + 3 + + +""" + + +def test_parse_model() -> None: + model = GetTargetsResponse.from_xml(data) + assert not model.model_extra