diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index ae47ad99..c9050b02 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -1,10 +1,30 @@ -# Release Notes +~~# Release Notes Welcome to the release notes for the `pan-scm-sdk` tool. This document provides a detailed record of changes, enhancements, and fixes in each version of the tool. --- +## Version 0.1.8 + +**Release Date:** October 16, 2024 + +### Anti Spyware Profiles + +- **Pytests**: Add the pytests to support Anti Spyware Profiles. + +--- + +## Version 0.1.7 + +**Release Date:** October 16, 2024 + +### Anti Spyware Profiles + +- **Anti Spyware Profiles**: Add the ability to support Anti Spyware Profiles. + +--- + ## Version 0.1.6 **Release Date:** October 15, 2024 diff --git a/docs/mermaid/sdk.mmd b/docs/mermaid/sdk.mmd index dbd4efb0..63c4fee6 100644 --- a/docs/mermaid/sdk.mmd +++ b/docs/mermaid/sdk.mmd @@ -1,330 +1,170 @@ classDiagram direction BT - class BaseException { - args - __cause__ - __context__ - __suppress_context__ - __traceback__ - __init__(self, *args: object) - __setstate__(self, __state: dict[str, Any] | None) - with_traceback(self, __tb: TracebackType | None) - } + class BaseException class Exception - class object { - __doc__ - __dict__ - __module__ - __annotations__ - __class__(self) - __class__(self, __type: type[object]) - __init__(self) - __new__(cls) - __setattr__(self, __name: str, __value: Any) - __delattr__(self, __name: str) - __eq__(self, __value: object) - __ne__(self, __value: object) - __str__(self) - __repr__(self) - __hash__(self) - __format__(self, __format_spec: str) - __getattribute__(self, __name: str) - __sizeof__(self) - __reduce__(self) - __reduce_ex__(self, __protocol: SupportsIndex) - __dir__(self) - __init_subclass__(cls) - __subclasshook__(cls, __subclass: type) - } - class node3 { - auth_request - signing_key - session - __init__(self, auth_request: AuthRequest) - _create_session(self) - _get_signing_key(self) - decode_token(self) - is_expired(self) - refresh_token(self) - } - class node6 { - session - api_base_url - oauth_client - __init__( - self, - client_id: str, - client_secret: str, - tsg_id: str, - api_base_url: str = "https://api.strata.paloaltonetworks.com", - ) - request(self, method: str, endpoint: str, **kwargs) - get(self, endpoint: str, **kwargs) - post(self, endpoint: str, **kwargs) - put(self, endpoint: str, **kwargs) - delete(self, endpoint: str, **kwargs) - } - class node1 { - client_id - client_secret - tsg_id - scope - token_url - construct_scope(cls, values) - } - class node5 { - parameters_str - combined_parameters - parent_namespace - __pydantic_generic_metadata__ - config_wrapper - original_model_post_init - parent_parameters - BaseModel - types_namespace - class_vars - error_message - __pydantic_decorators__ - model_computed_fields - base_private_attributes - private_attributes - base_field_names - cls - mro - generic_type_label - __pydantic_complete__ - __pydantic_post_init__ - missing_parameters - bases_str - __pydantic_custom_init__ - parameters - __pydantic_parent_namespace__ - __new__( - mcs, - cls_name: str, - bases: tuple[type[Any], ...], - namespace: dict[str, Any], - __pydantic_generic_metadata__: PydanticGenericMetadata | None = None, - __pydantic_reset_parent_namespace__: bool = True, - _create_model_module: str | None = None, - **kwargs: Any, - ) - __getattr__(self, item: str) - __prepare__(cls, *args: Any, **kwargs: Any) - __instancecheck__(self, instance: Any) - _collect_bases_data(bases: tuple[type[Any], ...]) - __fields__(self) - __dir__(self) - } - class node4 { - __pydantic_parent_namespace__ - model_config - model_fields - model_computed_fields - __class_vars__ - __private_attributes__ - __signature__ - __pydantic_complete__ - __pydantic_core_schema__ - __pydantic_custom_init__ - __pydantic_decorators__ - __pydantic_generic_metadata__ - __pydantic_parent_namespace__ - __pydantic_post_init__ - __pydantic_root_model__ - __pydantic_serializer__ - __pydantic_validator__ - __pydantic_extra__ - __pydantic_fields_set__ - __pydantic_private__ - __pydantic_core_schema__ - __pydantic_validator__ - __pydantic_serializer__ - __slots__ - __pydantic_base_init__ - __repr_name__ - __repr_str__ - __pretty__ - __rich_repr__ - __init__(self, /, **data: Any) - model_extra(self) - model_fields_set(self) - model_construct(cls, _fields_set: set[str] | None = None, **values: Any) - model_copy(self, *, update: dict[str, Any] | None = None, deep: bool = False) - model_dump( - self, - *, - mode: Literal['json', 'python'] | str = 'python', - include: IncEx | None = None, - exclude: IncEx | None = None, - context: Any | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - round_trip: bool = False, - warnings: bool | Literal['none', 'warn', 'error'] = True, - serialize_as_any: bool = False, - ) - model_dump_json( - self, - *, - indent: int | None = None, - include: IncEx | None = None, - exclude: IncEx | None = None, - context: Any | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - round_trip: bool = False, - warnings: bool | Literal['none', 'warn', 'error'] = True, - serialize_as_any: bool = False, - ) - model_json_schema( - cls, - by_alias: bool = True, - ref_template: str = DEFAULT_REF_TEMPLATE, - schema_generator: type[GenerateJsonSchema] = GenerateJsonSchema, - mode: JsonSchemaMode = 'validation', - ) - model_parametrized_name(cls, params: tuple[type[Any], ...]) - model_post_init(self, __context: Any) - model_rebuild( - cls, - *, - force: bool = False, - raise_errors: bool = True, - _parent_namespace_depth: int = 2, - _types_namespace: dict[str, Any] | None = None, - ) - model_validate( - cls, - obj: Any, - *, - strict: bool | None = None, - from_attributes: bool | None = None, - context: Any | None = None, - ) - model_validate_json( - cls, - json_data: str | bytes | bytearray, - *, - strict: bool | None = None, - context: Any | None = None, - ) - model_validate_strings( - cls, - obj: Any, - *, - strict: bool | None = None, - context: Any | None = None, - ) - __get_pydantic_core_schema__(cls, source: type[BaseModel], handler: GetCoreSchemaHandler, /) - __get_pydantic_json_schema__( - cls, - core_schema: CoreSchema, - handler: GetJsonSchemaHandler, - /, - ) - __pydantic_init_subclass__(cls, **kwargs: Any) - __class_getitem__( - cls, typevar_values: type[Any] | tuple[type[Any], ...] - ) - __copy__(self) - __deepcopy__(self, memo: dict[int, Any] | None = None) - __getattr__(self, item: str) - __setattr__(self, name: str, value: Any) - __delattr__(self, item: str) - _check_frozen(self, name: str, value: Any) - __getstate__(self) - __setstate__(self, state: dict[Any, Any]) - __eq__(self, other: Any) - __init_subclass__(cls, **kwargs: Unpack[ConfigDict]) - __iter__(self) - __repr__(self) - __repr_args__(self) - __str__(self) - __fields__(self) - __fields_set__(self) - dict(# noqa: D102 - self, - *, - include: IncEx | None = None, - exclude: IncEx | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) - json(# noqa: D102 - self, - *, - include: IncEx | None = None, - exclude: IncEx | None = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - encoder: Callable[[Any], Any] | None = PydanticUndefined, # type: ignore[assignment] - models_as_dict: bool = PydanticUndefined, # type: ignore[assignment] - **dumps_kwargs: Any, - ) - parse_obj(cls, obj: Any) - parse_raw(# noqa: D102 - cls, - b: str | bytes, - *, - content_type: str | None = None, - encoding: str = 'utf8', - proto: DeprecatedParseProtocol | None = None, - allow_pickle: bool = False, - ) - parse_file(# noqa: D102 - cls, - path: str | Path, - *, - content_type: str | None = None, - encoding: str = 'utf8', - proto: DeprecatedParseProtocol | None = None, - allow_pickle: bool = False, - ) - from_orm(cls, obj: Any) - construct(cls, _fields_set: set[str] | None = None, **values: Any) - copy( - self, - *, - include: AbstractSetIntStr | MappingIntStrAny | None = None, - exclude: AbstractSetIntStr | MappingIntStrAny | None = None, - update: Dict[str, Any] | None = None, # noqa UP006 - deep: bool = False, - ) - schema(# noqa: D102 - cls, by_alias: bool = True, ref_template: str = DEFAULT_REF_TEMPLATE - ) - schema_json(# noqa: D102 - cls, *, by_alias: bool = True, ref_template: str = DEFAULT_REF_TEMPLATE, **dumps_kwargs: Any - ) - validate(cls, value: Any) - update_forward_refs(cls, **localns: Any) - _iter(self, *args: Any, **kwargs: Any) - _copy_and_set_values(self, *args: Any, **kwargs: Any) - _get_value(cls, *args: Any, **kwargs: Any) - _calculate_keys(self, *args: Any, **kwargs: Any) - } - class node2 { - __hash__(self) - } - class node7 { - __iter__(self) - } + class node68 + class node29 + class object + class node36 + class node21 + class node44 + class node75 + class node78 + class node35 + class node6 + class node69 + class node67 + class node62 + class node70 + class node32 + class node13 + class node28 + class node79 + class node5 + class node25 + class node45 + class node66 + class node46 + class node10 + class node59 + class node14 + class node60 + class node9 + class node73 + class node34 + class node41 + class node56 + class node20 + class node47 + class node33 + class node54 + class node49 + class node52 + class node51 + class node42 + class node57 + class node63 + class node8 + class node30 + class node2 + class node1 + class node22 + class node50 + class node55 + class node11 + class node15 + class node26 + class node64 + class node72 + class node4 + class node65 + class node27 + class node18 + class node77 + class node76 + class node39 + class node40 + class node37 + class node71 + class node24 + class node12 + class node23 + class node53 + class node38 + class node74 + class str + class node31 + class node58 + class node61 + class node7 + class node43 + class node16 + class node3 object --> BaseException BaseException --> Exception - node2 ..> object - object --> node3 - object --> node6 - node4 --> node1 - object --> node4 - node5 "isinstanceof" ..> node4 - node7 ..> node4 + node29 "isinstanceof" ..> node68 + object --> node68 + node61 ..> node68 + node7 ..> node29 + node43 ..> node29 + node3 ..> node29 + node61 ..> object + object --> node21 + node36 "isinstanceof" ..> node21 + node7 ..> node21 + node21 --> node44 + object --> node75 + object --> node78 + object --> node35 + node35 --> node6 + node35 --> node69 + node35 --> node67 + node35 --> node62 + node35 --> node70 + node35 --> node32 + Exception --> node13 + node59 --> node28 + node13 --> node79 + node13 --> node5 + node13 --> node25 + node13 --> node45 + node25 --> node66 + node25 --> node46 + node25 --> node10 + node13 --> node59 + node25 --> node14 + node13 --> node60 + node45 --> node9 + node25 --> node73 + node45 --> node34 + node13 --> node41 + node79 --> node56 + node13 --> node20 + node13 --> node47 + node13 --> node33 + node21 --> node54 + node21 --> node49 + node21 --> node52 + node21 --> node51 + node21 --> node42 + node21 --> node57 + node21 --> node63 + node21 --> node8 + node21 --> node30 + node21 --> node2 + node21 --> node1 + node21 --> node22 + node21 --> node50 + node21 --> node55 + node21 --> node11 + node21 --> node15 + node44 --> node26 + node44 --> node64 + node21 --> node72 + node72 --> node4 + node72 --> node65 + node21 --> node27 + node21 --> node18 + node68 --> node77 + str --> node77 + node21 --> node76 + node68 --> node39 + str --> node39 + node21 --> node40 + node68 --> node37 + str --> node37 + node21 --> node71 + node71 --> node24 + node71 --> node12 + node68 --> node23 + str --> node23 + node21 --> node53 + node53 --> node38 + node53 --> node74 + node61 ..> str + node43 ..> str + node3 --> str + node58 --> node31 + node7 --> node31 + node7 --> node16 diff --git a/docs/sdk/address.md b/docs/sdk/config/objects/address.md similarity index 95% rename from docs/sdk/address.md rename to docs/sdk/config/objects/address.md index cff82138..8e37a9e9 100644 --- a/docs/sdk/address.md +++ b/docs/sdk/config/objects/address.md @@ -147,5 +147,5 @@ for addr in addresses: ## Related Models -- [AddressRequestModel](models/address_models.md#addressrequestmodel) -- [AddressResponseModel](models/address_models.md#addressresponsemodel) +- [AddressRequestModel](../../models/objects/address_models.md#addressrequestmodel) +- [AddressResponseModel](../../models/objects/address_models.md#addressresponsemodel) diff --git a/docs/sdk/address_group.md b/docs/sdk/config/objects/address_group.md similarity index 95% rename from docs/sdk/address_group.md rename to docs/sdk/config/objects/address_group.md index 7624d272..a10b1b94 100644 --- a/docs/sdk/address_group.md +++ b/docs/sdk/config/objects/address_group.md @@ -163,5 +163,5 @@ for group in groups: ## Related Models -- [AddressGroupRequestModel](models/address_group_models.md#addressgrouprequestmodel) -- [AddressGroupResponseModel](models/address_group_models.md#addressgroupresponsemodel) +- [AddressGroupRequestModel](../../models/objects/address_group_models.md#addressgrouprequestmodel) +- [AddressGroupResponseModel](../../models/objects/address_group_models.md#addressgroupresponsemodel) diff --git a/docs/sdk/application.md b/docs/sdk/config/objects/application.md similarity index 95% rename from docs/sdk/application.md rename to docs/sdk/config/objects/application.md index 39971e2e..600b89aa 100644 --- a/docs/sdk/application.md +++ b/docs/sdk/config/objects/application.md @@ -173,5 +173,5 @@ for app in applications: ## Related Models -- [ApplicationRequestModel](models/application_models.md#applicationrequestmodel) -- [ApplicationResponseModel](models/application_models.md#applicationresponsemodel) +- [ApplicationRequestModel](../../models/objects/application_models.md#applicationrequestmodel) +- [ApplicationResponseModel](../../models/objects/application_models.md#applicationresponsemodel) diff --git a/docs/sdk/application_group.md b/docs/sdk/config/objects/application_group.md similarity index 94% rename from docs/sdk/application_group.md rename to docs/sdk/config/objects/application_group.md index 3d0fcc88..f6eae9b2 100644 --- a/docs/sdk/application_group.md +++ b/docs/sdk/config/objects/application_group.md @@ -155,5 +155,5 @@ print(f"Created application group with ID: {new_group.id}") ## Related Models -- [ApplicationGroupRequestModel](models/application_group_models.md#ApplicationGrouprequestmodel) -- [ApplicationGroupResponseModel](models/application_group_models.md#ApplicationGroupresponsemodel) +- [ApplicationGroupRequestModel](../../models/objects/application_group_models.md#ApplicationGrouprequestmodel) +- [ApplicationGroupResponseModel](../../models/objects/application_group_models.md#ApplicationGroupresponsemodel) diff --git a/docs/sdk/configuration_objects.md b/docs/sdk/config/objects/index.md similarity index 97% rename from docs/sdk/configuration_objects.md rename to docs/sdk/config/objects/index.md index 28e3b71d..e2e6ba3a 100644 --- a/docs/sdk/configuration_objects.md +++ b/docs/sdk/config/objects/index.md @@ -1,4 +1,4 @@ -# Configuration Objects +# Objects This section covers the configuration objects provided by the `pan-scm-sdk`: diff --git a/docs/sdk/service.md b/docs/sdk/config/objects/service.md similarity index 95% rename from docs/sdk/service.md rename to docs/sdk/config/objects/service.md index 391742c0..c3b587a9 100644 --- a/docs/sdk/service.md +++ b/docs/sdk/config/objects/service.md @@ -168,7 +168,7 @@ for svc in services: ## Related Models -- [ServiceRequestModel](models/service_models.md#servicerequestmodel) -- [ServiceResponseModel](models/service_models.md#serviceresponsemodel) +- [ServiceRequestModel](../../models/objects/service_models.md#servicerequestmodel) +- [ServiceResponseModel](../../models/objects/service_models.md#serviceresponsemodel) --- diff --git a/docs/sdk/config/security_services/anti_spyware.md b/docs/sdk/config/security_services/anti_spyware.md new file mode 100644 index 00000000..d6ca6f56 --- /dev/null +++ b/docs/sdk/config/security_services/anti_spyware.md @@ -0,0 +1,169 @@ +# Anti-Spyware Profile Configuration Object + +The `AntiSpywareProfile` class is used to manage anti-spyware profile objects in the Strata Cloud Manager. It provides +methods to create, retrieve, update, delete, and list anti-spyware profile objects. + +--- + +## Importing the AntiSpywareProfile Class + +```python +from scm.config.security import AntiSpywareProfile +``` + +## Methods + +### `create(data: Dict[str, Any]) -> AntiSpywareProfileResponseModel` + +Creates a new anti-spyware profile object. + +**Parameters:** + +- `data` (Dict[str, Any]): A dictionary containing the anti-spyware profile object data. + +**Example:** + +```python +profile_data = { + "name": "test_profile", + "description": "Test anti-spyware profile", + "folder": "Prisma Access", + "rules": [ + { + "name": "rule1", + "severity": ["critical", "high"], + "category": "spyware", + "action": {"alert": {}} + } + ] +} + +new_profile = anti_spyware_profile.create(profile_data) +print(f"Created anti-spyware profile with ID: {new_profile.id}") +``` + +### `get(object_id: str) -> AntiSpywareProfileResponseModel` + +Retrieves an anti-spyware profile object by its ID. + +**Parameters:** + +- `object_id` (str): The UUID of the anti-spyware profile object. + +**Example:** + +```python +profile_id = "123e4567-e89b-12d3-a456-426655440000" +profile_object = anti_spyware_profile.get(profile_id) +print(f"Anti-Spyware Profile Name: {profile_object.name}") +``` + +### `update(object_id: str, data: Dict[str, Any]) -> AntiSpywareProfileResponseModel` + +Updates an existing anti-spyware profile object. + +**Parameters:** + +- `object_id` (str): The UUID of the anti-spyware profile object. +- `data` (Dict[str, Any]): A dictionary containing the updated anti-spyware profile data. + +**Example:** + +```python +update_data = { + "description": "Updated anti-spyware profile description", +} + +updated_profile = anti_spyware_profile.update(profile_id, update_data) +print(f"Updated anti-spyware profile with ID: {updated_profile.id}") +``` + +### `delete(object_id: str) -> None` + +Deletes an anti-spyware profile object by its ID. + +**Parameters:** + +- `object_id` (str): The UUID of the anti-spyware profile object. + +**Example:** + +```python +anti_spyware_profile.delete(profile_id) +print(f"Deleted anti-spyware profile with ID: {profile_id}") +``` + +### + +`list(folder: Optional[str] = None, snippet: Optional[str] = None, device: Optional[str] = None, offset: Optional[int] = None, limit: Optional[int] = None, name: Optional[str] = None, **filters) -> List[AntiSpywareProfileResponseModel]` + +Lists anti-spyware profile objects, optionally filtered by folder, snippet, device, or other criteria. + +**Parameters:** + +- `folder` (Optional[str]): The folder to list anti-spyware profiles from. +- `snippet` (Optional[str]): The snippet to list anti-spyware profiles from. +- `device` (Optional[str]): The device to list anti-spyware profiles from. +- `offset` (Optional[int]): The offset for pagination. +- `limit` (Optional[int]): The limit for pagination. +- `name` (Optional[str]): Filter profiles by name. +- `**filters`: Additional filters. + +**Example:** + +```python +profiles = anti_spyware_profile.list(folder='Prisma Access', limit=10) + +for profile in profiles: + print(f"Anti-Spyware Profile Name: {profile.name}, ID: {profile.id}") +``` + +--- + +## Usage Example + +```python +from scm.client import Scm +from scm.config.security import AntiSpywareProfile + +# Initialize the SCM client +scm = Scm( + client_id="your_client_id", + client_secret="your_client_secret", + tsg_id="your_tsg_id", +) + +# Create an AntiSpywareProfile instance +anti_spyware_profile = AntiSpywareProfile(scm) + +# Create a new anti-spyware profile +profile_data = { + "name": "test_profile", + "description": "Test anti-spyware profile", + "folder": "Prisma Access", + "rules": [ + { + "name": "rule1", + "severity": ["critical", "high"], + "category": "spyware", + "action": {"alert": {}} + } + ] +} + +new_profile = anti_spyware_profile.create(profile_data) +print(f"Created anti-spyware profile with ID: {new_profile.id}") + +# List anti-spyware profiles +profiles = anti_spyware_profile.list(folder='Prisma Access', limit=10) +for profile in profiles: + print(f"Anti-Spyware Profile Name: {profile.name}, ID: {profile.id}") +``` + +--- + +## Related Models + +- [AntiSpywareProfileRequestModel](../../models/security_services/anti_spyware_profile_models.md#AntiSpywareProfileRequestModel) +- [AntiSpywareProfileResponseModel](../../models/security_services/anti_spyware_profile_models.md#AntiSpywareProfileResponseModel) + diff --git a/docs/sdk/config/security_services/index.md b/docs/sdk/config/security_services/index.md new file mode 100644 index 00000000..d5a6b2be --- /dev/null +++ b/docs/sdk/config/security_services/index.md @@ -0,0 +1,20 @@ +# Security Services + +This section covers the configuration security services provided by the `pan-scm-sdk`: + +- [Anti Spyware Profile](anti_spyware.md) + +Each configuration object corresponds to a resource in the Strata Cloud Manager and provides methods for CRUD (Create, +Read, Update, Delete) operations. + +--- + +## Available Objects + +### [AntiSpywareProfile](anti_spyware.md) + +Manage individual Anti-Spyware Security Profiles. + +--- + +Select an object above to view detailed documentation, including methods, parameters, and examples. diff --git a/docs/sdk/index.md b/docs/sdk/index.md index 4da31533..a9914b3b 100644 --- a/docs/sdk/index.md +++ b/docs/sdk/index.md @@ -5,18 +5,24 @@ configuration objects and data models used to interact with Palo Alto Networks S ## Contents -- [Configuration Objects](configuration_objects.md) - - [Address](address.md) - - [Address Group](address_group.md) - - [Application](application.md) - - [Application Group](application_group.md) - - [Service](service.md) -- [Data Models](models.md) - - [Address Models](models/address_models.md) - - [Address Group Models](models/address_group_models.md) - - [Application Models](models/application_models.md) - - [Application Group Models](models/application_group_models.md) - - [Service Models](models/service_models.md) +- Configuration + - [Objects](config/objects/index) + - [Address](config/objects/address.md) + - [Address Group](config/objects/address_group.md) + - [Application](config/objects/application.md) + - [Application Group](config/objects/application_group.md) + - [Service](config/objects/service.md) + - [Security Services](config/security_services/index) + - [Anti-Spyware](config/security_services/anti_spyware.md) +- Data Models + - [Objects](models/objects/index) + - [Address Models](models/objects/address_models.md) + - [Address Group Models](models/objects/address_group_models.md) + - [Application Models](models/objects/application_models.md) + - [Application Group Models](models/objects/application_group_models.md) + - [Service Models](models/objects/service_models.md) + - [Security Services](models/security_services/index) + - [Anti-Spyware](models/security_services/anti_spyware_profile_models.md) --- @@ -25,5 +31,8 @@ configuration objects and data models used to interact with Palo Alto Networks S The `pan-scm-sdk` provides a set of classes and models to simplify interaction with the Strata Cloud Manager API. By utilizing this SDK, developers can programmatically manage configurations, ensuring consistency and efficiency. -Proceed to the [Configuration Objects](configuration_objects.md) section to learn more about the objects you can manage -using the SDK. +Proceed to the [Configuration Objects](config/objects/index) section to learn more about the objects you can +manage using the SDK. + +Proceed to the [Data Models](config/objects/index) section to learn more about how the Python dictionaries that are +passed into the SDK are structured. diff --git a/docs/sdk/models/address_group_models.md b/docs/sdk/models/objects/address_group_models.md similarity index 100% rename from docs/sdk/models/address_group_models.md rename to docs/sdk/models/objects/address_group_models.md diff --git a/docs/sdk/models/address_models.md b/docs/sdk/models/objects/address_models.md similarity index 100% rename from docs/sdk/models/address_models.md rename to docs/sdk/models/objects/address_models.md diff --git a/docs/sdk/models/application_group_models.md b/docs/sdk/models/objects/application_group_models.md similarity index 100% rename from docs/sdk/models/application_group_models.md rename to docs/sdk/models/objects/application_group_models.md diff --git a/docs/sdk/models/application_models.md b/docs/sdk/models/objects/application_models.md similarity index 100% rename from docs/sdk/models/application_models.md rename to docs/sdk/models/objects/application_models.md diff --git a/docs/sdk/models.md b/docs/sdk/models/objects/index.md similarity index 66% rename from docs/sdk/models.md rename to docs/sdk/models/objects/index.md index 8018df2b..0c484a93 100644 --- a/docs/sdk/models.md +++ b/docs/sdk/models/objects/index.md @@ -16,8 +16,8 @@ For each configuration object, there are corresponding request and response mode ## Models by Configuration Object -- [Address Models](models/address_models.md) -- [Address Group Models](models/address_group_models.md) -- [Application Models](models/application_models.md) -- [Application Group Models](models/application_group_models.md) -- [Service Models](models/service_models.md) +- [Address Models](address_models.md) +- [Address Group Models](address_group_models.md) +- [Application Models](application_models.md) +- [Application Group Models](application_group_models.md) +- [Service Models](service_models.md) diff --git a/docs/sdk/models/service_models.md b/docs/sdk/models/objects/service_models.md similarity index 100% rename from docs/sdk/models/service_models.md rename to docs/sdk/models/objects/service_models.md diff --git a/docs/sdk/models/security_services/anti_spyware_profile_models.md b/docs/sdk/models/security_services/anti_spyware_profile_models.md new file mode 100644 index 00000000..42ded69e --- /dev/null +++ b/docs/sdk/models/security_services/anti_spyware_profile_models.md @@ -0,0 +1,138 @@ +# Anti-Spyware Profile Models + +This section covers the data models associated with the `AntiSpywareProfile` configuration object. + +--- + +## AntiSpywareProfileRequestModel + +Used when creating or updating an anti-spyware profile object. + +### Attributes + +- `name` (str): **Required.** The name of the anti-spyware profile. +- `description` (Optional[str]): A description of the anti-spyware profile. +- `cloud_inline_analysis` (Optional[bool]): Enable or disable cloud inline analysis. Defaults to False. +- `inline_exception_edl_url` (Optional[List[str]]): List of inline exception EDL URLs. +- `inline_exception_ip_address` (Optional[List[str]]): List of inline exception IP addresses. +- `mica_engine_spyware_enabled` (Optional[List[MicaEngineSpywareEnabledEntry]]): List of MICA engine spyware enabled + entries. +- **Container Type Fields** (Exactly one must be provided): + - `folder` (Optional[str]): The folder where the profile is defined. + - `snippet` (Optional[str]): The snippet where the profile is defined. + - `device` (Optional[str]): The device where the profile is defined. +- `rules` (List[RuleRequest]): **Required.** List of rules for the profile. +- `threat_exception` (Optional[List[ThreatExceptionRequest]]): List of threat exceptions for the profile. + +### Example + +```python +anti_spyware_profile_request = AntiSpywareProfileRequestModel( + name="test_profile", + description="Test anti-spyware profile", + folder="Prisma Access", + rules=[ + RuleRequest( + name="rule1", + severity=["critical", "high"], + category="spyware", + action=ActionRequest(root={"alert": {}}) + ) + ] +) +``` + +--- + +## AntiSpywareProfileResponseModel + +Used when parsing anti-spyware profile objects retrieved from the API. + +### Attributes + +- `id` (str): The UUID of the anti-spyware profile object. +- `name` (str): The name of the anti-spyware profile. +- `description` (Optional[str]): A description of the anti-spyware profile. +- `cloud_inline_analysis` (Optional[bool]): Cloud inline analysis setting. +- `inline_exception_edl_url` (Optional[List[str]]): List of inline exception EDL URLs. +- `inline_exception_ip_address` (Optional[List[str]]): List of inline exception IP addresses. +- `mica_engine_spyware_enabled` (Optional[List[MicaEngineSpywareEnabledEntry]]): List of MICA engine spyware enabled + entries. +- **Container Type Fields**: + - `folder` (Optional[str]): The folder where the profile is defined. + - `snippet` (Optional[str]): The snippet where the profile is defined. + - `device` (Optional[str]): The device where the profile is defined. +- `rules` (List[RuleResponse]): List of rules for the profile. +- `threat_exception` (Optional[List[ThreatExceptionResponse]]): List of threat exceptions for the profile. + +### Example + +```python +anti_spyware_profile_response = AntiSpywareProfileResponseModel( + id="123e4567-e89b-12d3-a456-426655440000", + name="test_profile", + description="Test anti-spyware profile", + folder="Prisma Access", + rules=[ + RuleResponse( + name="rule1", + severity=["critical", "high"], + category="spyware", + action=ActionResponse(root={"alert": {}}) + ) + ] +) +``` + +--- + +## Additional Models + +### MicaEngineSpywareEnabledEntry + +Represents an entry in the 'mica_engine_spyware_enabled' list. + +#### Attributes + +- `name` (str): Name of the MICA engine spyware detector. +- `inline_policy_action` (InlinePolicyAction): Action to be taken by the inline policy. + +### RuleRequest and RuleResponse + +Represents a rule in the anti-spyware profile. + +#### Attributes + +- `name` (str): Rule name. +- `severity` (List[Severity]): List of severities. +- `category` (Category): Category of the rule. +- `threat_name` (Optional[str]): Threat name. +- `packet_capture` (Optional[PacketCapture]): Packet capture setting. +- `action` (ActionRequest or ActionResponse): Action to be taken. + +### ThreatExceptionRequest and ThreatExceptionResponse + +Represents a threat exception in the anti-spyware profile. + +#### Attributes + +- `name` (str): Threat exception name. +- `packet_capture` (PacketCapture): Packet capture setting. +- `exempt_ip` (Optional[List[ExemptIpEntry]]): Exempt IP list. +- `notes` (Optional[str]): Notes. +- `action` (ActionRequest or ActionResponse): Action to be taken. + +### ActionRequest and ActionResponse + +Represents the 'action' field in rules and threat exceptions. + +#### Methods + +- `get_action_name() -> str`: Returns the name of the action. + +### Enums + +- `InlinePolicyAction`: Enumeration of allowed inline policy actions. +- `PacketCapture`: Enumeration of packet capture options. +- `Severity`: Enumeration of severity levels. +- `Category`: Enumeration of threat categories. diff --git a/docs/sdk/models/security_services/index.md b/docs/sdk/models/security_services/index.md new file mode 100644 index 00000000..7225810e --- /dev/null +++ b/docs/sdk/models/security_services/index.md @@ -0,0 +1,19 @@ +# Data Models + +The `pan-scm-sdk` utilizes Pydantic models for data validation and serialization. This ensures that the data being sent +to and received from the Strata Cloud Manager API adheres to the expected structure and constraints. + +--- + +## Overview + +For each configuration object, there are corresponding request and response models: + +- **Request Models**: Used when creating or updating resources. +- **Response Models**: Used when parsing data retrieved from the API. + +--- + +## Models by Configuration Object + +- [Anti Spyware Security Profile Models](anti_spyware_profile_models.md) diff --git a/mkdocs.yml b/mkdocs.yml index a270cdb2..7ec0b1c7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,19 +26,28 @@ nav: - Getting Started: about/getting-started.md - SDK Developer Documentation: - Overview: sdk/index.md - - Configuration Objects: - - Address: sdk/address.md - - Address Group: sdk/address_group.md - - Application: sdk/application.md - - Application Group: sdk/application_group.md - - Service: sdk/service.md + - Configuration: + - Objects: + - Overview: sdk/config/objects/index.md + - Address: sdk/config/objects/address.md + - Address Group: sdk/config/objects/address_group.md + - Application: sdk/config/objects/application.md + - Application Group: sdk/config/objects/application_group.md + - Service: sdk/config/objects/service.md + - Security Services: + - Overview: sdk/config/security_services/index.md + - Address: sdk/config/security_services/anti_spyware.md - Data Models: - - Overview: sdk/models.md - - Address Models: sdk/models/address_models.md - - Address Group Models: sdk/models/address_group_models.md - - Application Models: sdk/models/application_models.md - - Application Group Models: sdk/models/application_group_models.md - - Service Models: sdk/models/service_models.md + - Objects: + - Overview: sdk/models/objects/index.md + - Address Models: sdk/models/objects/address_models.md + - Address Group Models: sdk/models/objects/address_group_models.md + - Application Models: sdk/models/objects/application_models.md + - Application Group Models: sdk/models/objects/application_group_models.md + - Service Models: sdk/models/objects/service_models.md + - Security Services: + - Overview: sdk/models/security_services/index.md + - Address: sdk/models/security_services/anti_spyware.md - Authentication Module: sdk/auth.md - SCM Client: sdk/client.md - Troubleshooting: about/troubleshooting.md diff --git a/pyproject.toml b/pyproject.toml index d3e0f793..690aa31b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pan-scm-sdk" -version = "0.1.6" +version = "0.1.8" description = "Python SDK for Palo Alto Networks Strata Cloud Manager." authors = ["Calvin Remsburg "] license = "Apache 2.0" diff --git a/scm/config/objects/address.py b/scm/config/objects/address.py index beec5353..7a9cc983 100644 --- a/scm/config/objects/address.py +++ b/scm/config/objects/address.py @@ -2,7 +2,7 @@ from typing import List, Dict, Any, Optional from scm.config import BaseObject -from scm.models import AddressRequestModel, AddressResponseModel +from scm.models.objects import AddressRequestModel, AddressResponseModel from scm.exceptions import ValidationError diff --git a/scm/config/objects/address_group.py b/scm/config/objects/address_group.py index 984bdee1..980ef6c7 100644 --- a/scm/config/objects/address_group.py +++ b/scm/config/objects/address_group.py @@ -2,7 +2,7 @@ from typing import List, Dict, Any, Optional from scm.config import BaseObject -from scm.models import AddressGroupRequestModel, AddressGroupResponseModel +from scm.models.objects import AddressGroupRequestModel, AddressGroupResponseModel from scm.exceptions import ValidationError diff --git a/scm/config/objects/application.py b/scm/config/objects/application.py index 81ec6029..c9cfd7a2 100644 --- a/scm/config/objects/application.py +++ b/scm/config/objects/application.py @@ -2,7 +2,7 @@ from typing import List, Dict, Any, Optional from scm.config import BaseObject -from scm.models import ApplicationRequestModel, ApplicationResponseModel +from scm.models.objects import ApplicationRequestModel, ApplicationResponseModel from scm.exceptions import ValidationError diff --git a/scm/config/objects/application_group.py b/scm/config/objects/application_group.py index 64ca9abf..c796ff5f 100644 --- a/scm/config/objects/application_group.py +++ b/scm/config/objects/application_group.py @@ -3,7 +3,10 @@ from typing import List, Dict, Any, Optional from scm.config import BaseObject -from scm.models import ApplicationGroupRequestModel, ApplicationGroupResponseModel +from scm.models.objects import ( + ApplicationGroupRequestModel, + ApplicationGroupResponseModel, +) from scm.exceptions import ValidationError diff --git a/scm/config/objects/service.py b/scm/config/objects/service.py index a47fd90a..d3dbc5f1 100644 --- a/scm/config/objects/service.py +++ b/scm/config/objects/service.py @@ -3,7 +3,7 @@ from typing import List, Dict, Any, Optional from scm.config import BaseObject -from scm.models import ServiceRequestModel, ServiceResponseModel +from scm.models.objects import ServiceRequestModel, ServiceResponseModel from scm.exceptions import ValidationError diff --git a/scm/config/security/__init__.py b/scm/config/security/__init__.py new file mode 100644 index 00000000..844a64b6 --- /dev/null +++ b/scm/config/security/__init__.py @@ -0,0 +1,3 @@ +# scm/config/security/__init__.py + +from .anti_spyware_profiles import AntiSpywareProfile diff --git a/scm/config/security/anti_spyware_profiles.py b/scm/config/security/anti_spyware_profiles.py new file mode 100644 index 00000000..23439c7f --- /dev/null +++ b/scm/config/security/anti_spyware_profiles.py @@ -0,0 +1,123 @@ +# scm/config/security/anti_spyware_profiles.py + +from typing import List, Dict, Any, Optional +from scm.config import BaseObject +from scm.models.security import ( + AntiSpywareProfileRequestModel, + AntiSpywareProfileResponseModel, +) +from scm.exceptions import ValidationError + + +class AntiSpywareProfile(BaseObject): + """ + Manages Anti-Spyware Profiles in Palo Alto Networks' Strata Cloud Manager. + + This class provides methods to create, retrieve, update, delete, and list Anti-Spyware Profiles + using the Strata Cloud Manager API. It supports operations within folders, snippets, + or devices, and allows filtering of profiles based on various criteria. + + Attributes: + ENDPOINT (str): The API endpoint for Anti-Spyware Profile operations. + + Errors: + ValidationError: Raised when invalid container parameters are provided. + + Returns: + AntiSpywareProfileResponseModel: For create, get, and update methods. + List[AntiSpywareProfileResponseModel]: For the list method. + """ + + ENDPOINT = "/config/security/v1/anti-spyware-profiles" + + def __init__(self, api_client): + super().__init__(api_client) + + def create(self, data: Dict[str, Any]) -> AntiSpywareProfileResponseModel: + profile = AntiSpywareProfileRequestModel(**data) + payload = profile.model_dump(exclude_unset=True) + response = self.api_client.post(self.ENDPOINT, json=payload) + return AntiSpywareProfileResponseModel(**response) + + def get(self, object_id: str) -> AntiSpywareProfileResponseModel: + endpoint = f"{self.ENDPOINT}/{object_id}" + response = self.api_client.get(endpoint) + return AntiSpywareProfileResponseModel(**response) + + def update( + self, object_id: str, data: Dict[str, Any] + ) -> AntiSpywareProfileResponseModel: + profile = AntiSpywareProfileRequestModel(**data) + payload = profile.model_dump(exclude_unset=True) + endpoint = f"{self.ENDPOINT}/{object_id}" + response = self.api_client.put(endpoint, json=payload) + return AntiSpywareProfileResponseModel(**response) + + def delete(self, object_id: str) -> None: + endpoint = f"{self.ENDPOINT}/{object_id}" + self.api_client.delete(endpoint) + + def list( + self, + folder: Optional[str] = None, + snippet: Optional[str] = None, + device: Optional[str] = None, + offset: Optional[int] = None, + limit: Optional[int] = None, + name: Optional[str] = None, + **filters, + ) -> List[AntiSpywareProfileResponseModel]: + params = {} + error_messages = [] + + # Validate offset and limit + if offset is not None: + if not isinstance(offset, int) or offset < 0: + error_messages.append("Offset must be a non-negative integer") + if limit is not None: + if not isinstance(limit, int) or limit <= 0: + error_messages.append("Limit must be a positive integer") + + # If there are any validation errors, raise ValueError with all error messages + if error_messages: + raise ValueError(". ".join(error_messages)) + + # Include container type parameter + container_params = {"folder": folder, "snippet": snippet, "device": device} + provided_containers = { + k: v for k, v in container_params.items() if v is not None + } + + if len(provided_containers) != 1: + raise ValidationError( + "Exactly one of 'folder', 'snippet', or 'device' must be provided." + ) + + params.update(provided_containers) + + # Handle pagination parameters + if offset is not None: + params["offset"] = offset + if limit is not None: + params["limit"] = limit + + # Handle filters + if name is not None: + params["name"] = name + + # Include any additional filters provided + params.update( + { + k: v + for k, v in filters.items() + if v is not None + and k not in container_params + and k not in ["offset", "limit", "name"] + } + ) + + response = self.api_client.get(self.ENDPOINT, params=params) + profiles = [ + AntiSpywareProfileResponseModel(**item) for item in response.get("data", []) + ] + return profiles diff --git a/scm/models/__init__.py b/scm/models/__init__.py index ade78791..7ea1a101 100644 --- a/scm/models/__init__.py +++ b/scm/models/__init__.py @@ -1,10 +1,3 @@ # scm/models/__init__.py -from .address import AddressRequestModel, AddressResponseModel -from .address_group import AddressGroupRequestModel, AddressGroupResponseModel -from .application import ApplicationRequestModel, ApplicationResponseModel -from .application_group import ( - ApplicationGroupRequestModel, - ApplicationGroupResponseModel, -) -from .service import ServiceRequestModel, ServiceResponseModel +from .auth import AuthRequestModel diff --git a/scm/models/objects/__init__.py b/scm/models/objects/__init__.py new file mode 100644 index 00000000..aa2b57af --- /dev/null +++ b/scm/models/objects/__init__.py @@ -0,0 +1,13 @@ +# scm/models/objects/__init__.py + +from .address import AddressRequestModel, AddressResponseModel +from .address_group import AddressGroupRequestModel, AddressGroupResponseModel +from .application import ( + ApplicationRequestModel, + ApplicationResponseModel, +) +from .application_group import ( + ApplicationGroupRequestModel, + ApplicationGroupResponseModel, +) +from .service import ServiceRequestModel, ServiceResponseModel diff --git a/scm/models/address.py b/scm/models/objects/address.py similarity index 94% rename from scm/models/address.py rename to scm/models/objects/address.py index 861de25c..616413bb 100644 --- a/scm/models/address.py +++ b/scm/models/objects/address.py @@ -1,3 +1,5 @@ +# scm/models/objects/address.py + import uuid from typing import Optional, List @@ -273,19 +275,3 @@ def validate_address_type(self) -> "AddressRequestModel": "Exactly one of 'ip_netmask', 'ip_range', 'ip_wildcard', or 'fqdn' must be provided." ) return self - - @model_validator(mode="after") - def validate_container_type(self) -> "AddressRequestModel": - container_fields = [ - "folder", - "snippet", - "device", - ] - provided = [ - field for field in container_fields if getattr(self, field) is not None - ] - if len(provided) != 1: - raise ValueError( - "Exactly one of 'folder', 'snippet', or 'device' must be provided." - ) - return self diff --git a/scm/models/address_group.py b/scm/models/objects/address_group.py similarity index 93% rename from scm/models/address_group.py rename to scm/models/objects/address_group.py index 4d2d49e6..03dc6241 100644 --- a/scm/models/address_group.py +++ b/scm/models/objects/address_group.py @@ -1,4 +1,4 @@ -# scm/models/address_group.py +# scm/models/objects/address_group.py import uuid from typing import Optional, List @@ -255,19 +255,3 @@ def validate_address_group_type(self) -> "AddressGroupRequestModel": if len(provided) != 1: raise ValueError("Exactly one of 'static' or 'dynamic' must be provided.") return self - - @model_validator(mode="after") - def validate_container_type(self) -> "AddressGroupRequestModel": - container_fields = [ - "folder", - "snippet", - "device", - ] - provided = [ - field for field in container_fields if getattr(self, field) is not None - ] - if len(provided) != 1: - raise ValueError( - "Exactly one of 'folder', 'snippet', or 'device' must be provided." - ) - return self diff --git a/scm/models/application.py b/scm/models/objects/application.py similarity index 99% rename from scm/models/application.py rename to scm/models/objects/application.py index 7c7226bc..945c3776 100644 --- a/scm/models/application.py +++ b/scm/models/objects/application.py @@ -1,4 +1,4 @@ -# scm/models/application.py +# scm/models/objects/application.py from typing import Optional, List from uuid import UUID diff --git a/scm/models/application_filter.py b/scm/models/objects/application_filter.py similarity index 100% rename from scm/models/application_filter.py rename to scm/models/objects/application_filter.py diff --git a/scm/models/application_group.py b/scm/models/objects/application_group.py similarity index 99% rename from scm/models/application_group.py rename to scm/models/objects/application_group.py index eae8deca..f1f20e0d 100644 --- a/scm/models/application_group.py +++ b/scm/models/objects/application_group.py @@ -1,4 +1,4 @@ -# scm/models/application_group.py +# scm/models/objects/application_group.py import uuid from typing import Optional, List diff --git a/scm/models/service.py b/scm/models/objects/service.py similarity index 99% rename from scm/models/service.py rename to scm/models/objects/service.py index d43c9610..21abac71 100644 --- a/scm/models/service.py +++ b/scm/models/objects/service.py @@ -1,4 +1,4 @@ -# scm/models/service.py +# scm/models/objects/service.py import uuid from typing import Optional, List diff --git a/scm/models/security/__init__.py b/scm/models/security/__init__.py new file mode 100644 index 00000000..6bd4a2ac --- /dev/null +++ b/scm/models/security/__init__.py @@ -0,0 +1,6 @@ +# scm/models/security/__init__.py + +from .anti_spyware_profiles import ( + AntiSpywareProfileRequestModel, + AntiSpywareProfileResponseModel, +) diff --git a/scm/models/security/anti_spyware_profiles.py b/scm/models/security/anti_spyware_profiles.py new file mode 100644 index 00000000..e19dd806 --- /dev/null +++ b/scm/models/security/anti_spyware_profiles.py @@ -0,0 +1,437 @@ +# scm/models/security/anti_spyware_profiles.py + +from typing import List, Optional +from pydantic import ( + BaseModel, + Field, + model_validator, + field_validator, + ConfigDict, + RootModel, +) +from enum import Enum +import uuid + + +class InlinePolicyAction(str, Enum): + """Enumeration of allowed inline policy actions.""" + + alert = "alert" + allow = "allow" + drop = "drop" + reset_both = "reset-both" + reset_client = "reset-client" + reset_server = "reset-server" + + +class MicaEngineSpywareEnabledEntry(BaseModel): + """ + Represents an entry in the 'mica_engine_spyware_enabled' list. + + Attributes: + name (str): Name of the MICA engine spyware detector. + inline_policy_action (InlinePolicyAction): Action to be taken by the inline policy. + """ + + name: str = Field( + ..., + description="Name of the MICA engine spyware detector", + ) + inline_policy_action: InlinePolicyAction = Field( + InlinePolicyAction.alert, + description="Inline policy action, defaults to 'alert'", + ) + + +class BlockIpAction(BaseModel): + """ + Represents the 'block_ip' action with additional properties. + + Attributes: + track_by (str): Method of tracking ('source-and-destination' or 'source'). + duration (int): Duration in seconds (1 to 3600). + """ + + track_by: str = Field( + ..., + description="Tracking method", + pattern="^(source-and-destination|source)$", + ) + duration: int = Field( + ..., + description="Duration in seconds", + ge=1, + le=3600, + ) + + +class ActionRequest(RootModel[dict]): + """ + Represents the 'action' field in rules and threat exceptions for requests. + + Enforces that exactly one action is provided. + """ + + @model_validator(mode="before") + @classmethod + def check_and_transform_action(cls, values): + if isinstance(values, str): + # Convert string to dict + values = {values: {}} + elif not isinstance(values, dict): + raise ValueError("Invalid action format; must be a string or dict.") + + action_fields = [ + "allow", + "alert", + "drop", + "reset_client", + "reset_server", + "reset_both", + "block_ip", + "default", + ] + provided_actions = [field for field in action_fields if field in values] + + if len(provided_actions) != 1: + raise ValueError("Exactly one action must be provided in 'action' field.") + + return values + + def get_action_name(self) -> str: + return next(iter(self.root.keys()), "unknown") + + +class ActionResponse(RootModel[dict]): + """ + Represents the 'action' field in rules and threat exceptions for responses. + + Accepts empty dictionaries. + """ + + @model_validator(mode="before") + @classmethod + def check_action(cls, values): + if isinstance(values, str): + # Convert string to dict + values = {values: {}} + elif not isinstance(values, dict): + raise ValueError("Invalid action format; must be a string or dict.") + + action_fields = [ + "allow", + "alert", + "drop", + "reset_client", + "reset_server", + "reset_both", + "block_ip", + "default", + ] + provided_actions = [field for field in action_fields if field in values] + + if len(provided_actions) > 1: + raise ValueError("At most one action must be provided in 'action' field.") + + # Accept empty dicts (no action specified) + return values + + def get_action_name(self) -> str: + return next(iter(self.root.keys()), "unknown") + + +class PacketCapture(str, Enum): + """Enumeration of packet capture options.""" + + disable = "disable" + single_packet = "single-packet" + extended_capture = "extended-capture" + + +class Severity(str, Enum): + """Enumeration of severity levels.""" + + critical = "critical" + high = "high" + medium = "medium" + low = "low" + informational = "informational" + any = "any" + + +class Category(str, Enum): + """Enumeration of threat categories.""" + + dns_proxy = "dns-proxy" + backdoor = "backdoor" + data_theft = "data-theft" + autogen = "autogen" + spyware = "spyware" + dns_security = "dns-security" + downloader = "downloader" + dns_phishing = "dns-phishing" + phishing_kit = "phishing-kit" + cryptominer = "cryptominer" + hacktool = "hacktool" + dns_benign = "dns-benign" + dns_wildfire = "dns-wildfire" + botnet = "botnet" + dns_grayware = "dns-grayware" + inline_cloud_c2 = "inline-cloud-c2" + keylogger = "keylogger" + p2p_communication = "p2p-communication" + domain_edl = "domain-edl" + webshell = "webshell" + command_and_control = "command-and-control" + dns_ddns = "dns-ddns" + net_worm = "net-worm" + tls_fingerprint = "tls-fingerprint" + dns_new_domain = "dns-new-domain" + dns = "dns" + fraud = "fraud" + dns_c2 = "dns-c2" + adware = "adware" + post_exploitation = "post-exploitation" + dns_malware = "dns-malware" + browser_hijack = "browser-hijack" + dns_parked = "dns-parked" + any = "any" + + +class ExemptIpEntry(BaseModel): + """ + Represents an entry in the 'exempt_ip' list within a threat exception. + + Attributes: + name (str): Name of the IP address or range to exempt. + """ + + name: str = Field( + ..., + description="Exempt IP name", + ) + + +class RuleBase(BaseModel): + """ + Base class for Rule. + """ + + name: str = Field( + ..., + description="Rule name", + ) + severity: List[Severity] = Field( + ..., + description="List of severities", + ) + category: Category = Field( + ..., + description="Category", + ) + threat_name: Optional[str] = Field( + None, + description="Threat name", + min_length=3, + ) + packet_capture: Optional[PacketCapture] = Field( + None, + description="Packet capture setting", + ) + + @field_validator("threat_name", mode="before") + @classmethod + def default_threat_name(cls, v): + return v or "any" + + +class RuleRequest(RuleBase): + action: Optional[ActionRequest] = Field( + None, + description="Action", + ) + + +class RuleResponse(RuleBase): + action: Optional[ActionResponse] = Field( + None, + description="Action", + ) + + +class ThreatExceptionBase(BaseModel): + """ + Base class for ThreatException. + """ + + name: str = Field( + ..., + description="Threat exception name", + ) + packet_capture: PacketCapture = Field( + ..., + description="Packet capture setting", + ) + exempt_ip: Optional[List[ExemptIpEntry]] = Field( + None, + description="Exempt IP list", + ) + notes: Optional[str] = Field( + None, + description="Notes", + ) + + +class ThreatExceptionRequest(ThreatExceptionBase): + action: ActionRequest = Field( + ..., + description="Action", + ) + + +class ThreatExceptionResponse(ThreatExceptionBase): + action: ActionResponse = Field( + ..., + description="Action", + ) + + +class AntiSpywareProfileBaseModel(BaseModel): + """ + Base model for AntiSpywareProfile, containing common fields. + """ + + name: str = Field( + ..., + description="Profile name", + ) + description: Optional[str] = Field( + None, + description="Description", + ) + cloud_inline_analysis: Optional[bool] = Field( + False, + description="Cloud inline analysis", + ) + inline_exception_edl_url: Optional[List[str]] = Field( + None, + description="Inline exception EDL URLs", + ) + inline_exception_ip_address: Optional[List[str]] = Field( + None, + description="Inline exception IP addresses", + ) + mica_engine_spyware_enabled: Optional[List[MicaEngineSpywareEnabledEntry]] = Field( + None, + description="List of MICA engine spyware enabled entries", + ) + + +class AntiSpywareProfileRequestModel(AntiSpywareProfileBaseModel): + """ + Represents an anti-spyware profile for API requests. + """ + + folder: Optional[str] = Field( + None, + description="Folder", + max_length=64, + pattern=r"^[a-zA-Z\d\-_. ]+$", + ) + snippet: Optional[str] = Field( + None, + description="Snippet", + max_length=64, + pattern=r"^[a-zA-Z\d\-_. ]+$", + ) + device: Optional[str] = Field( + None, + description="Device", + max_length=64, + pattern=r"^[a-zA-Z\d\-_. ]+$", + ) + rules: List[RuleRequest] = Field( + ..., + description="List of rules", + ) + threat_exception: Optional[List[ThreatExceptionRequest]] = Field( + None, + description="List of threat exceptions", + ) + + model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) + + @model_validator(mode="after") + def validate_container(self) -> "AntiSpywareProfileRequestModel": + container_fields = [ + "folder", + "snippet", + "device", + ] + provided_containers = [ + field for field in container_fields if getattr(self, field) is not None + ] + + if len(provided_containers) != 1: + raise ValueError( + "Exactly one of 'folder', 'snippet', or 'device' must be provided." + ) + + return self + + +class AntiSpywareProfileResponseModel(AntiSpywareProfileBaseModel): + """ + Represents an anti-spyware profile for API responses. + """ + + id: str = Field( + ..., + description="Profile ID", + ) + folder: Optional[str] = Field( + None, + description="Folder", + ) + snippet: Optional[str] = Field( + None, + description="Snippet", + ) + device: Optional[str] = Field( + None, + description="Device", + ) + rules: List[RuleResponse] = Field( + ..., + description="List of rules", + ) + threat_exception: Optional[List[ThreatExceptionResponse]] = Field( + None, + description="List of threat exceptions", + ) + + @field_validator("id") + @classmethod + def validate_id(cls, v): + try: + uuid.UUID(v) + except ValueError: + raise ValueError("Invalid UUID format for 'id'") + return v + + +class AntiSpywareProfilesResponse(BaseModel): + """ + Represents the API response containing a list of anti-spyware profiles. + + Attributes: + data (List[AntiSpywareProfileResponseModel]): List of anti-spyware profiles. + offset (int): Offset used in pagination. + total (int): Total number of profiles available. + limit (int): Maximum number of profiles returned. + """ + + data: List[AntiSpywareProfileResponseModel] + offset: int + total: int + limit: int diff --git a/tests/factories.py b/tests/factories.py index b69122c0..132d80c8 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -2,13 +2,13 @@ import factory -from scm.models import ( +from scm.models.objects import ( ApplicationRequestModel, ServiceRequestModel, ApplicationGroupRequestModel, ) -from scm.models.address import AddressRequestModel -from scm.models.address_group import AddressGroupRequestModel, DynamicFilter +from scm.models.objects.address import AddressRequestModel +from scm.models.objects.address_group import AddressGroupRequestModel, DynamicFilter class AddressFactory(factory.Factory): diff --git a/tests/test_address_groups.py b/tests/test_address_groups.py index f8fea947..f048f55d 100644 --- a/tests/test_address_groups.py +++ b/tests/test_address_groups.py @@ -3,8 +3,8 @@ from scm.config.objects import AddressGroup from scm.exceptions import ValidationError -from scm.models import AddressGroupResponseModel, AddressGroupRequestModel -from scm.models.address_group import DynamicFilter +from scm.models.objects import AddressGroupResponseModel, AddressGroupRequestModel +from scm.models.objects.address_group import DynamicFilter from tests.factories import AddressGroupStaticFactory, AddressGroupDynamicFactory from unittest.mock import MagicMock diff --git a/tests/test_addresses.py b/tests/test_addresses.py index 6a0f36e3..9c8bcb01 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -1,9 +1,11 @@ # tests/test_addresses.py +import uuid + import pytest from scm.config.objects import Address from scm.exceptions import ValidationError -from scm.models import AddressResponseModel, AddressRequestModel +from scm.models.objects import AddressResponseModel, AddressRequestModel from tests.factories import AddressFactory from unittest.mock import MagicMock @@ -388,3 +390,96 @@ def test_address_request_model_multiple_containers_with_device(): assert "Exactly one of 'folder', 'snippet', or 'device' must be provided." in str( exc_info.value ) + + +def test_address_request_model_validation(): + """ + Test validation in AddressRequestModel. + """ + # Valid input with exactly one address type + valid_data = { + "name": "TestAddress", + "ip_netmask": "192.168.1.1/24", + "folder": "Shared", + } + address = AddressRequestModel(**valid_data) + assert address.name == "TestAddress" + assert address.ip_netmask == "192.168.1.1/24" + + # No address type provided + invalid_data_no_address = { + "name": "InvalidAddress", + "folder": "Shared", + } + with pytest.raises(ValueError) as exc_info: + AddressRequestModel(**invalid_data_no_address) + assert ( + "Exactly one of 'ip_netmask', 'ip_range', 'ip_wildcard', or 'fqdn' must be provided." + in str(exc_info.value) + ) + + # Multiple address types provided + invalid_data_multiple_addresses = { + "name": "InvalidAddress", + "ip_netmask": "192.168.1.1/24", + "fqdn": "example.com", + "folder": "Shared", + } + with pytest.raises(ValueError) as exc_info: + AddressRequestModel(**invalid_data_multiple_addresses) + assert ( + "Exactly one of 'ip_netmask', 'ip_range', 'ip_wildcard', or 'fqdn' must be provided." + in str(exc_info.value) + ) + + # Test each address type individually + address_types = ["ip_netmask", "ip_range", "ip_wildcard", "fqdn"] + for address_type in address_types: + valid_data = { + "name": f"TestAddress_{address_type}", + address_type: "test_value", + "folder": "Shared", + } + address = AddressRequestModel(**valid_data) + assert getattr(address, address_type) == "test_value" + + # Test valid UUID + valid_uuid = str(uuid.uuid4()) + valid_uuid_data = { + "id": valid_uuid, + "name": "ValidUUIDAddress", + "ip_netmask": "192.168.1.1/24", + "folder": "Shared", + } + address = AddressResponseModel(**valid_uuid_data) + assert address.id == valid_uuid + + # Test invalid UUID + invalid_uuid_data = { + "id": "invalid-uuid", + "name": "InvalidUUIDAddress", + "ip_netmask": "192.168.1.1/24", + "folder": "Shared", + } + with pytest.raises(ValueError) as exc_info: + AddressRequestModel(**invalid_uuid_data) + assert "Invalid UUID format for 'id'" in str(exc_info.value) + + # Test with None UUID + none_uuid_data = { + "id": None, + "name": "NoneUUIDAddress", + "ip_netmask": "192.168.1.1/24", + "folder": "Shared", + } + address = AddressResponseModel(**none_uuid_data) + assert address.id is None + + # Test without UUID field + no_uuid_data = { + "name": "NoUUIDAddress", + "ip_netmask": "192.168.1.1/24", + "folder": "Shared", + } + address = AddressRequestModel(**no_uuid_data) + assert not hasattr(address, "id") diff --git a/tests/test_anti_spyware_profiles.py b/tests/test_anti_spyware_profiles.py new file mode 100644 index 00000000..bbe4465a --- /dev/null +++ b/tests/test_anti_spyware_profiles.py @@ -0,0 +1,634 @@ +# tests/test_anti_spyware_profiles.py + +import pytest +from unittest.mock import MagicMock + +from scm.config.security.anti_spyware_profiles import AntiSpywareProfile +from scm.exceptions import ValidationError +from scm.models.security.anti_spyware_profiles import ( + AntiSpywareProfileRequestModel, + AntiSpywareProfileResponseModel, + RuleRequest, + RuleResponse, + ThreatExceptionRequest, + ThreatExceptionResponse, + Severity, + Category, + PacketCapture, + ActionRequest, + ActionResponse, +) +from typing import List + + +def test_list_anti_spyware_profiles(load_env, mock_scm): + """ + Test listing anti-spyware profiles. + """ + # Mock response from the API client + mock_response = { + "data": [ + { + "id": "123e4567-e89b-12d3-a456-426655440000", + "name": "TestProfile1", + "folder": "Prisma Access", + "description": "A test anti-spyware profile", + "rules": [ + { + "name": "TestRule1", + "severity": ["critical", "high"], + "category": "spyware", + "threat_name": "any", + "packet_capture": "disable", + "action": {"alert": {}}, + } + ], + "threat_exception": [ + { + "name": "TestException1", + "action": {"allow": {}}, + "packet_capture": "single-packet", + "exempt_ip": [{"name": "192.168.1.1"}], + "notes": "Test note", + } + ], + }, + { + "id": "223e4567-e89b-12d3-a456-426655440001", + "name": "TestProfile2", + "folder": "Prisma Access", + "rules": [], + "threat_exception": [], + }, + ], + "offset": 0, + "total": 2, + "limit": 200, + } + + # Mock the API client's get method + mock_scm.get = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the list method + profiles = anti_spyware_profile_client.list(folder="Prisma Access") + + # Assertions + mock_scm.get.assert_called_once_with( + "/config/security/v1/anti-spyware-profiles", params={"folder": "Prisma Access"} + ) + assert isinstance(profiles, list) + assert len(profiles) == 2 + assert isinstance(profiles[0], AntiSpywareProfileResponseModel) + assert profiles[0].name == "TestProfile1" + assert profiles[0].description == "A test anti-spyware profile" + assert profiles[0].rules[0].name == "TestRule1" + assert profiles[0].rules[0].severity == [Severity.critical, Severity.high] + assert profiles[0].rules[0].category == Category.spyware + assert profiles[0].rules[0].action.get_action_name() == "alert" + assert profiles[0].threat_exception[0].name == "TestException1" + assert profiles[0].threat_exception[0].action.get_action_name() == "allow" + assert profiles[0].threat_exception[0].exempt_ip[0].name == "192.168.1.1" + + +def test_create_anti_spyware_profile(load_env, mock_scm): + """ + Test creating an anti-spyware profile. + """ + # Prepare test data + test_profile_data = { + "name": "NewTestProfile", + "folder": "Prisma Access", + "description": "A new test anti-spyware profile", + "rules": [ + { + "name": "NewRule", + "severity": ["medium", "low"], + "category": "adware", + "packet_capture": "single-packet", + "action": "alert", + } + ], + "threat_exception": [ + { + "name": "NewException", + "action": "allow", + "packet_capture": "disable", + "exempt_ip": [{"name": "10.0.0.1"}], + "notes": "Exception note", + } + ], + } + + # Expected payload after model processing + expected_payload = { + "name": "NewTestProfile", + "folder": "Prisma Access", + "description": "A new test anti-spyware profile", + "rules": [ + { + "name": "NewRule", + "severity": ["medium", "low"], + "category": "adware", + "packet_capture": "single-packet", + "action": {"alert": {}}, + } + ], + "threat_exception": [ + { + "name": "NewException", + "action": {"allow": {}}, + "packet_capture": "disable", + "exempt_ip": [{"name": "10.0.0.1"}], + "notes": "Exception note", + } + ], + } + + # Mock response from the API client + mock_response = expected_payload.copy() + mock_response["id"] = "333e4567-e89b-12d3-a456-426655440002" # Mocked ID + + # Mock the API client's post method + mock_scm.post = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the create method + created_profile = anti_spyware_profile_client.create(test_profile_data) + + # Assertions + mock_scm.post.assert_called_once_with( + "/config/security/v1/anti-spyware-profiles", + json=expected_payload, + ) + assert isinstance(created_profile, AntiSpywareProfileResponseModel) + assert created_profile.id == "333e4567-e89b-12d3-a456-426655440002" + assert created_profile.name == "NewTestProfile" + assert created_profile.rules[0].name == "NewRule" + assert created_profile.rules[0].action.get_action_name() == "alert" + assert created_profile.threat_exception[0].name == "NewException" + + +def test_get_anti_spyware_profile(load_env, mock_scm): + """ + Test retrieving an anti-spyware profile by ID. + """ + # Mock response from the API client + profile_id = "123e4567-e89b-12d3-a456-426655440000" + mock_response = { + "id": profile_id, + "name": "TestProfile", + "folder": "Prisma Access", + "description": "A test anti-spyware profile", + "rules": [], + "threat_exception": [], + } + + # Mock the API client's get method + mock_scm.get = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the get method + profile = anti_spyware_profile_client.get(profile_id) + + # Assertions + mock_scm.get.assert_called_once_with( + f"/config/security/v1/anti-spyware-profiles/{profile_id}" + ) + assert isinstance(profile, AntiSpywareProfileResponseModel) + assert profile.id == profile_id + assert profile.name == "TestProfile" + assert profile.description == "A test anti-spyware profile" + + +def test_update_anti_spyware_profile(load_env, mock_scm): + """ + Test updating an anti-spyware profile. + """ + # Prepare test data + profile_id = "123e4567-e89b-12d3-a456-426655440000" + update_data = { + "name": "UpdatedProfile", + "folder": "Prisma Access", + "description": "An updated anti-spyware profile", + "rules": [ + { + "name": "UpdatedRule", + "severity": ["high"], + "category": "botnet", + "packet_capture": "extended-capture", + "action": {"block_ip": {"track_by": "source", "duration": 3600}}, + } + ], + "threat_exception": [], + } + + # Expected payload after model processing + expected_payload = { + "name": "UpdatedProfile", + "folder": "Prisma Access", + "description": "An updated anti-spyware profile", + "rules": [ + { + "name": "UpdatedRule", + "severity": ["high"], + "category": "botnet", + "packet_capture": "extended-capture", + "action": {"block_ip": {"track_by": "source", "duration": 3600}}, + } + ], + "threat_exception": [], + } + + # Mock response from the API client + mock_response = expected_payload.copy() + mock_response["id"] = profile_id + + # Mock the API client's put method + mock_scm.put = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the update method + updated_profile = anti_spyware_profile_client.update(profile_id, update_data) + + # Assertions + mock_scm.put.assert_called_once_with( + f"/config/security/v1/anti-spyware-profiles/{profile_id}", + json=expected_payload, + ) + assert isinstance(updated_profile, AntiSpywareProfileResponseModel) + assert updated_profile.id == profile_id + assert updated_profile.name == "UpdatedProfile" + assert updated_profile.description == "An updated anti-spyware profile" + assert updated_profile.rules[0].action.get_action_name() == "block_ip" + + +def test_delete_anti_spyware_profile(load_env, mock_scm): + """ + Test deleting an anti-spyware profile. + """ + # Prepare test data + profile_id = "123e4567-e89b-12d3-a456-426655440000" + + # Mock the API client's delete method + mock_scm.delete = MagicMock(return_value=None) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the delete method + anti_spyware_profile_client.delete(profile_id) + + # Assertions + mock_scm.delete.assert_called_once_with( + f"/config/security/v1/anti-spyware-profiles/{profile_id}" + ) + + +def test_anti_spyware_profile_list_validation_error(load_env, mock_scm): + """ + Test validation error when listing with multiple containers. + """ + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Attempt to call the list method with multiple containers + with pytest.raises(ValidationError) as exc_info: + anti_spyware_profile_client.list(folder="Shared", snippet="TestSnippet") + + # Assertions + assert "Exactly one of 'folder', 'snippet', or 'device' must be provided." in str( + exc_info.value + ) + + +def test_anti_spyware_profile_request_model_validation_errors(): + """ + Test validation errors in AntiSpywareProfileRequestModel. + """ + # No container provided + data_no_container = { + "name": "InvalidProfile", + "rules": [], + } + with pytest.raises(ValueError) as exc_info: + AntiSpywareProfileRequestModel(**data_no_container) + assert "Exactly one of 'folder', 'snippet', or 'device' must be provided." in str( + exc_info.value + ) + + # Multiple containers provided + data_multiple_containers = { + "name": "InvalidProfile", + "folder": "Shared", + "device": "Device1", + "rules": [], + } + with pytest.raises(ValueError) as exc_info: + AntiSpywareProfileRequestModel(**data_multiple_containers) + assert "Exactly one of 'folder', 'snippet', or 'device' must be provided." in str( + exc_info.value + ) + + # Invalid action in RuleRequest + data_invalid_action = { + "name": "InvalidProfile", + "folder": "Shared", + "rules": [ + { + "name": "InvalidRule", + "severity": ["high"], + "category": "botnet", + "action": {}, # Empty action dictionary + } + ], + } + with pytest.raises(ValueError) as exc_info: + AntiSpywareProfileRequestModel(**data_invalid_action) + assert "Exactly one action must be provided in 'action' field." in str( + exc_info.value + ) + + # Invalid UUID in id field (for response model) + data_invalid_id = { + "id": "invalid-uuid", + "name": "TestProfile", + "folder": "Shared", + "rules": [], + } + with pytest.raises(ValueError) as exc_info: + AntiSpywareProfileResponseModel(**data_invalid_id) + assert "Invalid UUID format for 'id'" in str(exc_info.value) + + +def test_rule_request_model_validation(): + """ + Test validation in RuleRequest model. + """ + # Invalid severity + data_invalid_severity = { + "name": "TestRule", + "severity": ["nonexistent_severity"], + "category": "spyware", + "action": "alert", + } + with pytest.raises(ValueError) as exc_info: + RuleRequest(**data_invalid_severity) + assert "1 validation error for RuleRequest" in str(exc_info.value) + + # Missing action + data_missing_action = { + "name": "TestRule", + "severity": ["critical"], + "category": "spyware", + } + rule = RuleRequest(**data_missing_action) + assert rule.action is None + + +def test_threat_exception_request_model_validation(): + """ + Test validation in ThreatExceptionRequest model. + """ + # Invalid packet_capture + data_invalid_packet_capture = { + "name": "TestException", + "action": "alert", + "packet_capture": "invalid_option", + } + with pytest.raises(ValueError) as exc_info: + ThreatExceptionRequest(**data_invalid_packet_capture) + assert "1 validation error for ThreatExceptionRequest" in str(exc_info.value) + + # Missing action + data_missing_action = { + "name": "TestException", + "packet_capture": "disable", + } + with pytest.raises(Exception) as exc_info: + ThreatExceptionRequest(**data_missing_action) + # assert isinstance(exc_info.value, ValidationError) + assert "1 validation error for ThreatExceptionRequest" in str(exc_info.value) + assert "action\n Field required" in str(exc_info.value) + + +def test_list_anti_spyware_profiles_with_pagination(load_env, mock_scm): + """ + Test listing anti-spyware profiles with pagination parameters. + """ + # Mock response from the API client + mock_response = { + "data": [ + { + "id": "223e4567-e89b-12d3-a456-426655440001", + "name": "TestProfile2", + "folder": "Prisma Access", + "rules": [], + "threat_exception": [], + }, + ], + "offset": 1, + "total": 2, + "limit": 1, + } + + # Mock the API client's get method + mock_scm.get = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the list method with pagination parameters + profiles = anti_spyware_profile_client.list( + folder="Prisma Access", offset=1, limit=1 + ) + + # Assertions + mock_scm.get.assert_called_once_with( + "/config/security/v1/anti-spyware-profiles", + params={"folder": "Prisma Access", "offset": 1, "limit": 1}, + ) + assert isinstance(profiles, list) + assert len(profiles) == 1 + assert profiles[0].name == "TestProfile2" + assert profiles[0].id == "223e4567-e89b-12d3-a456-426655440001" + + +def test_list_anti_spyware_profiles_with_name_filter(load_env, mock_scm): + """ + Test listing anti-spyware profiles with name filter. + """ + # Mock response from the API client + mock_response = { + "data": [ + { + "id": "223e4567-e89b-12d3-a456-426655440001", + "name": "SpecificProfile", + "folder": "Prisma Access", + "rules": [], + "threat_exception": [], + }, + ], + "offset": 0, + "total": 1, + "limit": 200, + } + + # Mock the API client's get method + mock_scm.get = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the list method with name filter + profiles = anti_spyware_profile_client.list( + folder="Prisma Access", name="SpecificProfile" + ) + + # Assertions + mock_scm.get.assert_called_once_with( + "/config/security/v1/anti-spyware-profiles", + params={"folder": "Prisma Access", "name": "SpecificProfile"}, + ) + assert isinstance(profiles, list) + assert len(profiles) == 1 + assert profiles[0].name == "SpecificProfile" + assert profiles[0].id == "223e4567-e89b-12d3-a456-426655440001" + + +def test_list_anti_spyware_profiles_with_all_parameters(load_env, mock_scm): + """ + Test listing anti-spyware profiles with all optional parameters. + """ + # Mock response from the API client + mock_response = { + "data": [], + "offset": 10, + "total": 2, + "limit": 5, + } + + # Mock the API client's get method + mock_scm.get = MagicMock(return_value=mock_response) + + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Call the list method with all optional parameters + profiles = anti_spyware_profile_client.list( + folder="Prisma Access", name="TestProfile", offset=10, limit=5 + ) + + # Assertions + mock_scm.get.assert_called_once_with( + "/config/security/v1/anti-spyware-profiles", + params={ + "folder": "Prisma Access", + "name": "TestProfile", + "offset": 10, + "limit": 5, + }, + ) + assert isinstance(profiles, list) + assert len(profiles) == 0 # As per the mocked response data + + +def test_anti_spyware_profile_list_with_invalid_pagination(load_env, mock_scm): + """ + Test validation error when invalid pagination parameters are provided. + """ + # Create an instance of AntiSpywareProfile with the mocked Scm + anti_spyware_profile_client = AntiSpywareProfile(mock_scm) + + # Attempt to call the list method with invalid pagination parameters + with pytest.raises(ValueError) as exc_info: + anti_spyware_profile_client.list( + folder="Prisma Access", + offset=-1, + limit=0, + ) + + # Assertions + assert "Offset must be a non-negative integer" in str(exc_info.value) + assert "Limit must be a positive integer" in str(exc_info.value) + + +def test_action_request_check_and_transform_action(): + # Test string input + action = ActionRequest("alert") + assert action.root == {"alert": {}} + + # Test dict input + action = ActionRequest({"drop": {}}) + assert action.root == {"drop": {}} + + # Test invalid input type + with pytest.raises( + ValueError, match="Invalid action format; must be a string or dict." + ): + ActionRequest(123) + + # Test multiple actions + with pytest.raises( + ValueError, match="Exactly one action must be provided in 'action' field." + ): + ActionRequest({"alert": {}, "drop": {}}) + + # Test empty dict + with pytest.raises( + ValueError, match="Exactly one action must be provided in 'action' field." + ): + ActionRequest({}) + + +def test_action_request_get_action_name(): + action = ActionRequest("alert") + assert action.get_action_name() == "alert" + + action = ActionRequest({"drop": {}}) + assert action.get_action_name() == "drop" + + +def test_action_response_check_action(): + # Test string input + action = ActionResponse("alert") + assert action.root == {"alert": {}} + + # Test dict input + action = ActionResponse({"drop": {}}) + assert action.root == {"drop": {}} + + # Test invalid input type + with pytest.raises( + ValueError, match="Invalid action format; must be a string or dict." + ): + ActionResponse(123) + + # Test multiple actions + with pytest.raises( + ValueError, match="At most one action must be provided in 'action' field." + ): + ActionResponse({"alert": {}, "drop": {}}) + + # Test empty dict (should be allowed for ActionResponse) + action = ActionResponse({}) + assert action.root == {} + + +def test_action_response_get_action_name(): + action = ActionResponse("alert") + assert action.get_action_name() == "alert" + + action = ActionResponse({"drop": {}}) + assert action.get_action_name() == "drop" + + action = ActionResponse({}) + assert action.get_action_name() == "unknown" diff --git a/tests/test_application_groups.py b/tests/test_application_groups.py index 333bbaac..744148db 100644 --- a/tests/test_application_groups.py +++ b/tests/test_application_groups.py @@ -4,7 +4,10 @@ from scm.config.objects import ApplicationGroup from scm.exceptions import ValidationError -from scm.models import ApplicationGroupResponseModel, ApplicationGroupRequestModel +from scm.models.objects import ( + ApplicationGroupResponseModel, + ApplicationGroupRequestModel, +) from tests.factories import ApplicationGroupFactory from unittest.mock import MagicMock diff --git a/tests/test_applications.py b/tests/test_applications.py index 2906c993..a599ab05 100644 --- a/tests/test_applications.py +++ b/tests/test_applications.py @@ -3,7 +3,7 @@ from scm.config.objects import Application from scm.exceptions import ValidationError -from scm.models import ApplicationResponseModel, ApplicationRequestModel +from scm.models.objects import ApplicationResponseModel, ApplicationRequestModel from tests.factories import ApplicationFactory from unittest.mock import MagicMock diff --git a/tests/test_models.py b/tests/test_models.py index 71cfa9ab..48ef5dad 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,11 +2,11 @@ import pytest -from scm.models import ( +from scm.models.objects import ( AddressRequestModel, AddressGroupRequestModel, ) -from scm.models.address_group import DynamicFilter +from scm.models.objects.address_group import DynamicFilter from pydantic import ValidationError from tests.factories import ( diff --git a/tests/test_services.py b/tests/test_services.py index 59f7a7e7..e8d75854 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -6,7 +6,7 @@ from scm.exceptions import ValidationError as SCMValidationError from scm.config.objects import Service -from scm.models import ServiceRequestModel, ServiceResponseModel +from scm.models.objects import ServiceRequestModel, ServiceResponseModel from tests.factories import ServiceFactory