diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47e0ed37..aad24aba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: isort args: ["--profile", "black"] - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/pycqa/flake8 diff --git a/zanshinsdk/client.py b/zanshinsdk/client.py index a9be4651..99e159b2 100644 --- a/zanshinsdk/client.py +++ b/zanshinsdk/client.py @@ -18,78 +18,45 @@ import httpx from pydantic import BaseModel, Field +from zanshinsdk.common.enums import ( + AlertSeverity, + AlertsOrderOpts, + AlertState, + Day, + Frequency, + Languages, + OAuthTargetKind, + Roles, + ScanTargetGroupKind, + ScanTargetKind, + SortOpts, + TimeOfDay, +) +from zanshinsdk.common.targets import ( + ScanTargetAWS, + ScanTargetAZURE, + ScanTargetBITBUCKET, + ScanTargetDOMAIN, + ScanTargetGCP, + ScanTargetGITHUB, + ScanTargetGITLAB, + ScanTargetGroupCredentialListORACLE, + ScanTargetGWORKSPACE, + ScanTargetHUAWEI, + ScanTargetJIRA, + ScanTargetMS365, + ScanTargetORACLE, + ScanTargetSALESFORCE, + ScanTargetSLACK, + ScanTargetZENDESK, +) +from zanshinsdk.common.validators import validate_class, validate_int, validate_uuid from zanshinsdk.version import __version__ as sdk_version CONFIG_DIR = Path.home() / ".tenchi" CONFIG_FILE = CONFIG_DIR / "config" -class AlertState(str, Enum): - OPEN = "OPEN" - ACTIVE = "ACTIVE" - IN_PROGRESS = "IN_PROGRESS" - RISK_ACCEPTED = "RISK_ACCEPTED" - MITIGATING_CONTROL = "MITIGATING_CONTROL" - FALSE_POSITIVE = "FALSE_POSITIVE" - CLOSED = "CLOSED" - - -class AlertSeverity(str, Enum): - CRITICAL = "CRITICAL" - HIGH = "HIGH" - MEDIUM = "MEDIUM" - LOW = "LOW" - INFO = "INFO" - - -class ScanTargetKind(str, Enum): - AWS = "AWS" - GCP = "GCP" - AZURE = "AZURE" - HUAWEI = "HUAWEI" - DOMAIN = "DOMAIN" - ORACLE = "ORACLE" - - -class AlertsOrderOpts(str, Enum): - SCAN_TARGET_ID = "scanTargetId" - RESOURCE = "resource" - RULE = "rule" - SEVERITY = "severity" - STATE = "state" - CREATED_AT = "createdAt" - UPDATED_AT = "updatedAt" - - -class SortOpts(str, Enum): - ASC = "asc" - DESC = "desc" - - -class Frequency(Enum): - SIX_HOURS = "6h" - TWELVE_HOURS = "12h" - DAILY = "1d" - WEEKLY = "7d" - - -class TimeOfDay(Enum): - MORNING = "MORNING" - AFTERNOON = "AFTERNOON" - EVENING = "EVENING" - NIGHT = "NIGHT" - - -class Day(Enum): - SUNDAY = "SUNDAY" - MONDAY = "MONDAY" - TUESDAY = "TUESDAY" - WEDNESDAY = "WEDNESDAY" - THURSDAY = "THURSDAY" - FRIDAY = "FRIDAY" - SATURDAY = "SATURDAY" - - class ScanTargetSchedule(BaseModel): frequency: Frequency time_of_day: Optional[TimeOfDay] = Field(TimeOfDay.NIGHT, alias="timeOfDay") @@ -119,69 +86,6 @@ def json(self): ) -class ScanTargetAWS(dict): - def __init__(self, account): - dict.__init__(self, account=account) - - -class ScanTargetAZURE(dict): - def __init__(self, application_id, subscription_id, directory_id, secret): - dict.__init__( - self, - applicationId=application_id, - subscriptionId=subscription_id, - directoryId=directory_id, - secret=secret, - ) - - -class ScanTargetGCP(dict): - def __init__(self, project_id): - dict.__init__(self, projectId=project_id) - - -class ScanTargetHUAWEI(dict): - def __init__(self, account_id): - dict.__init__(self, accountId=account_id) - - -class ScanTargetDOMAIN(dict): - def __init__(self, domain): - dict.__init__(self, domain=domain) - - -class ScanTargetORACLE(dict): - def __init__(self, compartment_id, region, tenancy_id, user_id, key_fingerprint): - dict.__init__( - self, - compartment_id=compartment_id, - region=region, - tenancy_id=tenancy_id, - user_id=user_id, - key_fingerprint=key_fingerprint, - ) - - -class ScanTargetGroupCredentialListORACLE(dict): - def __init__(self, region, tenancy_id, user_id, key_fingerprint): - dict.__init__( - self, - region=region, - tenancy_id=tenancy_id, - user_id=user_id, - key_fingerprint=key_fingerprint, - ) - - -class Roles(str, Enum): - ADMIN = "ADMIN" - - -class Languages(str, Enum): - PT_BR = "pt-BR" - EN_US = "en-US" - - class Client: def __init__( self, @@ -1016,6 +920,14 @@ def create_organization_scan_target( ScanTargetHUAWEI, ScanTargetDOMAIN, ScanTargetORACLE, + ScanTargetZENDESK, + ScanTargetGWORKSPACE, + ScanTargetSLACK, + ScanTargetBITBUCKET, + ScanTargetJIRA, + ScanTargetGITLAB, + ScanTargetSALESFORCE, + ScanTargetMS365, ], schedule: ScanTargetSchedule = DAILY, ) -> Dict: @@ -1030,24 +942,39 @@ def create_organization_scan_target( * For Azure scan targets, provide *applicationId*, *subscriptionId*, *directoryId* and *secret* fields. * For GCP scan targets, provide a *projectId* field * For DOMAIN scan targets, provide a URL in the *domain* field + * For ZENDESK scan target, provide *instance_url* field + * For Jira scan target, provide *jira_url* field + * For MS365 scan target, provide *tenant_id*, *application_id*, *secret* fields + * For GITHUB scan target, provide *installation_id*, *organizationName* fields + * For GWORKSPACE, SLACK, BITBUCKET, GITLAB, SALESFORCE no one credential are needed :param schedule: schedule as a string or enum version of the scan frequency :return: a dict representing the newly created scan target """ validate_class(kind, ScanTargetKind) validate_class(name, str) - if kind == ScanTargetKind.AWS: - validate_class(credential, ScanTargetAWS) - elif kind == ScanTargetKind.AZURE: - validate_class(credential, ScanTargetAZURE) - elif kind == ScanTargetKind.GCP: - validate_class(credential, ScanTargetGCP) - elif kind == ScanTargetKind.HUAWEI: - validate_class(credential, ScanTargetHUAWEI) - elif kind == ScanTargetKind.DOMAIN: - validate_class(credential, ScanTargetDOMAIN) - elif kind == ScanTargetKind.ORACLE: - validate_class(credential, ScanTargetORACLE) + validator_credential_map = { + ScanTargetKind.AWS: ScanTargetAWS, + ScanTargetKind.AZURE: ScanTargetAZURE, + ScanTargetKind.GCP: ScanTargetGCP, + ScanTargetKind.HUAWEI: ScanTargetHUAWEI, + ScanTargetKind.DOMAIN: ScanTargetDOMAIN, + ScanTargetKind.ORACLE: ScanTargetORACLE, + ScanTargetKind.ZENDESK: ScanTargetZENDESK, + ScanTargetKind.GWORKSPACE: ScanTargetGWORKSPACE, + ScanTargetKind.SLACK: ScanTargetSLACK, + ScanTargetKind.BITBUCKET: ScanTargetBITBUCKET, + ScanTargetKind.JIRA: ScanTargetJIRA, + ScanTargetKind.GITLAB: ScanTargetGITLAB, + ScanTargetKind.SALESFORCE: ScanTargetSALESFORCE, + ScanTargetKind.MS365: ScanTargetMS365, + ScanTargetKind.GITHUB: ScanTargetGITHUB, + } + + if not validator_credential_map.get(kind): + raise ValueError(f"Invalid kind: {kind}") + + validate_class(credential, validator_credential_map.get(kind)) body = { "name": name, @@ -1055,6 +982,7 @@ def create_organization_scan_target( "credential": credential, "schedule": schedule.value(), } + return self._request( "POST", f"/organizations/{validate_uuid(organization_id)}/scantargets", @@ -1178,6 +1106,46 @@ def check_organization_scan_target( f"{validate_uuid(scan_target_id)}/check", ).json() + ################################################### + # Scan Target OAuth + ################################################### + + def get_kind_oauth_link( + self, + organization_id: Union[UUID, str], + scan_target_id: Union[UUID, str], + kind: Union[ScanTargetKind, OAuthTargetKind], + ) -> Dict: + """ + Retrieve a link to authorize zanshin to read info from their target. + + Mandatory for scan targets of kind: + * ZENDESK + * GWORKSPACE + * SLACK + * BITBUCKET + * JIRA + * GITLAB + * SALESFORCE + :return: a dict with the link + """ + if kind.value not in [member.value for member in OAuthTargetKind]: + raise ValueError(f"{repr(kind.value)} is not eligible for OAuth link") + + scan_type = ( + "scanTargetGroupId" + if kind in [ScanTargetKind.BITBUCKET, ScanTargetKind.GITLAB] + else "scanTargetId" + ) + + path = ( + f"/oauth/link" + f"?organizationId={validate_uuid(organization_id)}" + f"&{scan_type}={validate_uuid(scan_target_id)}" + ) + + return self._request("GET", path).json() + def get_gworkspace_oauth_link( self, organization_id: Union[UUID, str], scan_target_id: Union[UUID, str] ) -> Dict: @@ -1278,15 +1246,18 @@ def create_scan_target_group( """ validate_class(kind, ScanTargetKind) validate_class(name, str) - if kind != ScanTargetKind.ORACLE: + group_kinds = [member.value for member in ScanTargetGroupKind] + + if kind not in group_kinds: raise ValueError( - f"{repr(kind.value)} is not accepted. 'ORACLE' is expected" + f"{repr(kind.value)} is not accepted. '{group_kinds}' is expected" ) body = { "name": name, "kind": kind, } + return self._request( "POST", f"/organizations/{validate_uuid(organization_id)}/scantargetgroups", @@ -2602,40 +2573,3 @@ def _check_boto3_installation(self): sys.modules[package_name] = module spec.loader.exec_module(module) return module - - -def validate_int( - value, min_value=None, max_value=None, required=False -) -> Optional[int]: - if value is None: - if required: - raise ValueError("required integer parameter missing") - else: - return value - if not isinstance(value, int): - raise TypeError(f"{repr(value)} is not an integer") - if min_value and value < min_value: - raise ValueError(f"{value} shouldn't be lower than {min_value}") - if max_value and value > max_value: - raise ValueError(f"{value} shouldn't be higher than {max_value}") - return value - - -def validate_class(value, class_type): - if not isinstance(value, class_type): - raise TypeError(f"{repr(value)} is not an instance of {class_type.__name__}") - return value - - -def validate_uuid(uuid: Union[UUID, str]) -> str: - try: - if isinstance(uuid, str): - return str(UUID(uuid)) - - if isinstance(uuid, UUID): - return str(uuid) - - raise TypeError - except (ValueError, TypeError) as ex: - ex.args = (f"{repr(uuid)} is not a valid UUID",) - raise ex diff --git a/zanshinsdk/common/__init__.py b/zanshinsdk/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zanshinsdk/common/enums.py b/zanshinsdk/common/enums.py new file mode 100644 index 00000000..043ddd42 --- /dev/null +++ b/zanshinsdk/common/enums.py @@ -0,0 +1,101 @@ +from enum import Enum + + +class AlertState(str, Enum): + OPEN = "OPEN" + ACTIVE = "ACTIVE" + IN_PROGRESS = "IN_PROGRESS" + RISK_ACCEPTED = "RISK_ACCEPTED" + MITIGATING_CONTROL = "MITIGATING_CONTROL" + FALSE_POSITIVE = "FALSE_POSITIVE" + CLOSED = "CLOSED" + + +class AlertSeverity(str, Enum): + CRITICAL = "CRITICAL" + HIGH = "HIGH" + MEDIUM = "MEDIUM" + LOW = "LOW" + INFO = "INFO" + + +class ScanTargetKind(str, Enum): + AWS = "AWS" + GCP = "GCP" + AZURE = "AZURE" + HUAWEI = "HUAWEI" + DOMAIN = "DOMAIN" + ORACLE = "ORACLE" + MS365 = "MS365" + GITHUB = "GITHUB" + ZENDESK = "ZENDESK" + GWORKSPACE = "GWORKSPACE" + SLACK = "SLACK" + BITBUCKET = "BITBUCKET" + JIRA = "JIRA" + GITLAB = "GITLAB" + SALESFORCE = "SALESFORCE" + + +class ScanTargetGroupKind(str, Enum): + ORACLE = "ORACLE" + BITBUCKET = "BITBUCKET" + GITLAB = "GITLAB" + + +class OAuthTargetKind(str, Enum): + ZENDESK = "ZENDESK" + GWORKSPACE = "GWORKSPACE" + SLACK = "SLACK" + BITBUCKET = "BITBUCKET" + JIRA = "JIRA" + GITLAB = "GITLAB" + SALESFORCE = "SALESFORCE" + + +class AlertsOrderOpts(str, Enum): + SCAN_TARGET_ID = "scanTargetId" + RESOURCE = "resource" + RULE = "rule" + SEVERITY = "severity" + STATE = "state" + CREATED_AT = "createdAt" + UPDATED_AT = "updatedAt" + + +class SortOpts(str, Enum): + ASC = "asc" + DESC = "desc" + + +class Frequency(Enum): + SIX_HOURS = "6h" + TWELVE_HOURS = "12h" + DAILY = "1d" + WEEKLY = "7d" + + +class TimeOfDay(Enum): + MORNING = "MORNING" + AFTERNOON = "AFTERNOON" + EVENING = "EVENING" + NIGHT = "NIGHT" + + +class Day(Enum): + SUNDAY = "SUNDAY" + MONDAY = "MONDAY" + TUESDAY = "TUESDAY" + WEDNESDAY = "WEDNESDAY" + THURSDAY = "THURSDAY" + FRIDAY = "FRIDAY" + SATURDAY = "SATURDAY" + + +class Roles(str, Enum): + ADMIN = "ADMIN" + + +class Languages(str, Enum): + PT_BR = "pt-BR" + EN_US = "en-US" diff --git a/zanshinsdk/common/targets.py b/zanshinsdk/common/targets.py new file mode 100644 index 00000000..86ff59a3 --- /dev/null +++ b/zanshinsdk/common/targets.py @@ -0,0 +1,104 @@ +class ScanTargetAWS(dict): + def __init__(self, account): + dict.__init__(self, account=account) + + +class ScanTargetAZURE(dict): + def __init__(self, application_id, subscription_id, directory_id, secret): + dict.__init__( + self, + applicationId=application_id, + subscriptionId=subscription_id, + directoryId=directory_id, + secret=secret, + ) + + +class ScanTargetGCP(dict): + def __init__(self, project_id): + dict.__init__(self, projectId=project_id) + + +class ScanTargetHUAWEI(dict): + def __init__(self, account_id): + dict.__init__(self, accountId=account_id) + + +class ScanTargetDOMAIN(dict): + def __init__(self, domain): + dict.__init__(self, domain=domain) + + +class ScanTargetORACLE(dict): + def __init__(self, compartment_id, region, tenancy_id, user_id, key_fingerprint): + dict.__init__( + self, + compartment_id=compartment_id, + region=region, + tenancy_id=tenancy_id, + user_id=user_id, + key_fingerprint=key_fingerprint, + ) + + +class ScanTargetGroupCredentialListORACLE(dict): + def __init__(self, region, tenancy_id, user_id, key_fingerprint): + dict.__init__( + self, + region=region, + tenancy_id=tenancy_id, + user_id=user_id, + key_fingerprint=key_fingerprint, + ) + + +class ScanTargetZENDESK(dict): + def __init__(self, instance_url): + dict.__init__(self, instanceUrl=instance_url) + + +class ScanTargetGWORKSPACE(dict): + def __init__(self): + dict.__init__(self) + + +class ScanTargetSLACK(dict): + def __init__(self): + dict.__init__(self) + + +class ScanTargetBITBUCKET(dict): + def __init__(self): + dict.__init__(self) + + +class ScanTargetJIRA(dict): + def __init__(self, jira_url): + dict.__init__(self, jiraUrl=jira_url) + + +class ScanTargetGITLAB(dict): + def __init__(self): + dict.__init__(self) + + +class ScanTargetSALESFORCE(dict): + def __init__(self): + dict.__init__(self) + + +class ScanTargetMS365(dict): + def __init__(self, application_id, tenant_id, secret): + dict.__init__( + self, + applicationId=application_id, + tenantId=tenant_id, + secret=secret, + ) + + +class ScanTargetGITHUB(dict): + def __init__(self, organization_name, installation_id): + dict.__init__( + self, organizationName=organization_name, installationId=installation_id + ) diff --git a/zanshinsdk/common/validators.py b/zanshinsdk/common/validators.py new file mode 100644 index 00000000..711a2139 --- /dev/null +++ b/zanshinsdk/common/validators.py @@ -0,0 +1,39 @@ +from typing import Optional, Union +from uuid import UUID + + +def validate_int( + value, min_value=None, max_value=None, required=False +) -> Optional[int]: + if value is None: + if required: + raise ValueError("required integer parameter missing") + else: + return value + if not isinstance(value, int): + raise TypeError(f"{repr(value)} is not an integer") + if min_value and value < min_value: + raise ValueError(f"{value} shouldn't be lower than {min_value}") + if max_value and value > max_value: + raise ValueError(f"{value} shouldn't be higher than {max_value}") + return value + + +def validate_class(value, class_type): + if not isinstance(value, class_type): + raise TypeError(f"{repr(value)} is not an instance of {class_type.__name__}") + return value + + +def validate_uuid(uuid: Union[UUID, str]) -> str: + try: + if isinstance(uuid, str): + return str(UUID(uuid)) + + if isinstance(uuid, UUID): + return str(uuid) + + raise TypeError + except (ValueError, TypeError) as ex: + ex.args = (f"{repr(uuid)} is not a valid UUID",) + raise ex diff --git a/zanshinsdk/tests/__init__.py b/zanshinsdk/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zanshinsdk/dummy_aws_credentials b/zanshinsdk/tests/data/dummy_aws_credentials similarity index 100% rename from zanshinsdk/dummy_aws_credentials rename to zanshinsdk/tests/data/dummy_aws_credentials diff --git a/zanshinsdk/dummy_cloudformation_zanshin_service_role_template.json b/zanshinsdk/tests/data/dummy_cloudformation_zanshin_service_role_template.json similarity index 100% rename from zanshinsdk/dummy_cloudformation_zanshin_service_role_template.json rename to zanshinsdk/tests/data/dummy_cloudformation_zanshin_service_role_template.json diff --git a/zanshinsdk/test_alerts_history.py b/zanshinsdk/tests/test_alerts_history.py similarity index 100% rename from zanshinsdk/test_alerts_history.py rename to zanshinsdk/tests/test_alerts_history.py diff --git a/zanshinsdk/test_client.py b/zanshinsdk/tests/test_client.py similarity index 98% rename from zanshinsdk/test_client.py rename to zanshinsdk/tests/test_client.py index 40c00b1d..17410c64 100644 --- a/zanshinsdk/test_client.py +++ b/zanshinsdk/tests/test_client.py @@ -167,7 +167,7 @@ def test_init_user_agent_from_config(self, mock_is_file): def mock_aws_credentials(self): """Mocked AWS Credentials for moto.""" moto_credentials_file_path = ( - Path(__file__).parent.absolute() / "dummy_aws_credentials" + Path(__file__).parent.absolute() / "data/dummy_aws_credentials" ) os.environ["AWS_SHARED_CREDENTIALS_FILE"] = str(moto_credentials_file_path) @@ -1031,6 +1031,42 @@ def test_check_organization_scan_target(self): f"/organizations/{organization_id}/scantargets/{scan_target_id}/check", ) + def test_get_kind_oauth_link_should_call_api_with_scan_target_id(self): + organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" + scan_target_id = "e22f4225-43e9-4922-b6b8-8b0620bdb110" + kind = zanshinsdk.ScanTargetKind.GITLAB + + self.sdk.get_kind_oauth_link(organization_id, scan_target_id, kind) + + self.sdk._request.assert_called_once_with( + "GET", + f"/oauth/link?" + f"organizationId={organization_id}" + f"&scanTargetGroupId={scan_target_id}", + ) + + def test_get_kind_oauth_link_should_call_api_with_scan_target_group_id(self): + organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" + scan_target_id = "e22f4225-43e9-4922-b6b8-8b0620bdb110" + kind = zanshinsdk.ScanTargetKind.JIRA + + self.sdk.get_kind_oauth_link(organization_id, scan_target_id, kind) + + self.sdk._request.assert_called_once_with( + "GET", + f"/oauth/link?" + f"organizationId={organization_id}" + f"&scanTargetId={scan_target_id}", + ) + + def test_get_kind_oauth_link_should_raise_exception_with_invalid_kind(self): + organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" + scan_target_id = "e22f4225-43e9-4922-b6b8-8b0620bdb110" + kind = zanshinsdk.ScanTargetKind.AWS + + with self.assertRaises(ValueError): + self.sdk.get_kind_oauth_link(organization_id, scan_target_id, kind) + def test_get_gworkspace_oauth_link(self): organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" scan_target_id = "e22f4225-43e9-4922-b6b8-8b0620bdb110" @@ -1213,24 +1249,27 @@ def test_update_scan_target_group(self): body={"name": name}, ) - def test_create_scan_target_group(self): + def test_create_scan_target_group_should_call_api_with_valid_kind(self): organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" - kind = zanshinsdk.ScanTargetKind.ORACLE + kind = zanshinsdk.ScanTargetKind.BITBUCKET name = "ScanTargetTest" self.sdk.create_scan_target_group(organization_id, kind, name) - with self.assertRaises(ValueError): - self.sdk.create_scan_target_group( - organization_id, zanshinsdk.ScanTargetKind.AWS, name - ) - self.sdk._request.assert_called_once_with( "POST", f"/organizations/{organization_id}/scantargetgroups", body={"name": name, "kind": kind}, ) + def test_create_scan_target_group_should_throw_exception_with_invalid_kind(self): + organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" + kind = zanshinsdk.ScanTargetKind.AWS + name = "ScanTargetTest" + + with self.assertRaises(ValueError): + self.sdk.create_scan_target_group(organization_id, kind, name) + def test_iter_scan_target_group_compartments(self): organization_id = "822f4225-43e9-4922-b6b8-8b0620bdb1e3" scan_target_group_id = "322f4225-43e9-4922-b6b8-8b0620bdb110" @@ -3054,7 +3093,8 @@ def test_onboard_scan_target_aws_boto3_profile(self, request, mock_is_file): # Create Mocked S3 tenchi-assets bucket with open( - "zanshinsdk/dummy_cloudformation_zanshin_service_role_template.json", "r" + "zanshinsdk/tests/data/dummy_cloudformation_zanshin_service_role_template.json", + "r", ) as dummy_template_file: DUMMY_TEMPLATE = json.load(dummy_template_file) s3 = boto3.client("s3", region_name="us-east-2") @@ -3176,7 +3216,8 @@ def test_onboard_scan_target_aws_boto3_session(self, request, mock_is_file): # Create Mocked S3 tenchi-assets bucket with open( - "zanshinsdk/dummy_cloudformation_zanshin_service_role_template.json", "r" + "zanshinsdk/tests/data/dummy_cloudformation_zanshin_service_role_template.json", + "r", ) as dummy_template_file: DUMMY_TEMPLATE = json.load(dummy_template_file) s3 = boto3.client("s3", region_name="us-east-2") diff --git a/zanshinsdk/test_following_alerts_history.py b/zanshinsdk/tests/test_following_alerts_history.py similarity index 100% rename from zanshinsdk/test_following_alerts_history.py rename to zanshinsdk/tests/test_following_alerts_history.py diff --git a/zanshinsdk/test_iterator.py b/zanshinsdk/tests/test_iterator.py similarity index 100% rename from zanshinsdk/test_iterator.py rename to zanshinsdk/tests/test_iterator.py