Skip to content

Commit

Permalink
Fetch assets within iris alert (#162)
Browse files Browse the repository at this point in the history
* Add endpoint to fetch assets by alert ID

* Update CaseModel state_id and state_name fields

* Add stack provisioning router

* Update rotate_empty_index_set field to be optional

* send sap siem to shuffle endpoint

* Add command to retrieve admin password

* some precommit fixes

* Refactor provision_wazuh_content_pack route in Graylog

* Add function to load content pack JSON file

* Add insert_content_pack function to Graylog content_packs service

* wazuh content pack change

* Fix bug in login functionality

* Update Wazuh Content Pack name

* Add logging for POST request response and implement content pack installation and graylog system checking to ensure graylog version supports the content pack

* add active-response readme

* active response testing

* Add windows firewall active response

* Refactor firewall script and add block_ip function

* precommit fixes

* Update Windows Firewall script to include IP unblocking

* Add wait_for_complete parameter to provision_wazuh_content_pack_route()

* Add firewall.spec to .gitignore

* Update active response command and add IP blocking action

* full path for netsh

* Fix Windows firewall script and update README.md

* Update Wazuh configuration in readme

* Update action to unblock IP and add agents list parameter

* add active response markdowns

* Add InvokeActiveResponseRequest model and update provision_wazuh_content_pack_route

* precommit fixes

* Refactor active response validation and alert creation

* precommit fixes

* precommit fixes

* README updates

* updated dependencies

* updated vite config

* Handle exception when creating customer in provision_wazuh_customer

* readme install python

* add to windows_firewall ar logging

* updated npm scripts

* updated assets types

* updated soc assets apis

* added soc alert assets component

* updated soc asset description

* ready for prod build

* precommit fixes

---------

Co-authored-by: Davide Di Modica <[email protected]>
  • Loading branch information
taylorwalton and Linko91 authored Feb 20, 2024
1 parent 20eee2c commit dc5fda1
Show file tree
Hide file tree
Showing 41 changed files with 5,996 additions and 229 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ backend/file-store/api.config.yaml
unplugin.components.d.ts
package-lock.json
*.checkpoint
firewall.spec
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
{
"cSpell.words": [
"ajoelp",
"apexchart",
"colord",
"datejs",
"datetimesec",
"echarts",
"Healthcheck",
"majesticons",
"mimecast",
"picocolors",
"redoc",
"rushstack",
"sparkline",
"taze",
"uvicorn",
"venv",
"Wazuh"
"Wazuh",
"xaxis"
]
}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ mkdir data

# Run Copilot
docker compose up -d

# Once Copilot has started up you can retrieve the admin password by running the following command (Only accessible the first time Copilot is started up)
docker logs "$(docker ps --filter ancestor=ghcr.io/socfortress/copilot-backend:latest --format "{{.ID}}")" 2>&1 | grep "Admin user password"
```

Copilot shall be available on the host interface, port 443, protocol HTTPS - `https://<your_instance_ip>`.
Expand Down
67 changes: 67 additions & 0 deletions backend/app/active_response/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Windows custom active response configuration

You can implement the custom Python script on Windows endpoints using two methods. The first method converts Python scripts to executable applications, while the second method uses a Windows Batch launcher to run the Python script.

Both methods require Python installed on the Windows endpoint. Use the following steps below to install Python on the Windows endpoint.

#. Download Python executable installer from the `official Python website <https://www.python.org/downloads/windows/>`\_\_.
#. Run the Python installer once downloaded. Check the following boxes when prompted and start the installation:

- **Use admin privileges when installing py.exe**.
- **Add python.exe to PATH**. This places the interpreter in the execution path.

Or you can use the following PowerShell command to install Python:

```
Invoke-WebRequest -Uri "https://www.python.org/ftp/python/3.11.0/python-3.11.0-amd64.exe" -OutFile "$env:TEMP\python-3.11.0-amd64.exe"; Start-Process -FilePath "$env:TEMP\python-3.11.0-amd64.exe" -ArgumentList "/quiet InstallAllUsers=1 PrependPath=1" -Wait -NoNewWindow
```

Method 1: Convert the Python script to an executable application
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""

#. Open an administrator PowerShell terminal and use `pip` to install `pyinstaller`:

> pip install pyinstaller
> pyinstaller --version

#. Run the following command using PowerShell with administrator privileges to create the executable file:

❗ - Make sure to point to the Wazuh DLLs

```powershell
pyinstaller --log-level DEBUG --add-data "C:\Program Files (x86)\ossec-agent\libwazuhext.dll;." --add-data "C:\Program Files (x86)\ossec-agent\libwinpthread-1.dll;." --add-data "C:\Program Files (x86)\ossec-agent\libwazuhshared.dll;." -F <PATH_TO_CUSTOM-AR.PY>
```

You can find the created `custom-ar.exe` executable in the `C:\Users\<USER>\dist\` directory.

#. Copy the `custom-ar.exe` executable file to `C:\Program Files (x86)\ossec-agent\active-response\bin\` directory on the monitored endpoint.
#. Restart the Wazuh agent using PowerShell with administrator privileges to apply the changes:

.. code-block:: console

> Restart-Service -Name wazuh

#. On the Wazuh server, add the `<command>` and `<active-response>` blocks below to the `/var/ossec/etc/ossec.conf` configuration file. This uses the `custom-ar.exe` executable for Windows endpoints.

```xml
<command>
<name>windows_firewall</name>
<executable>windows_firewall.exe</executable>
<timeout_allowed>no</timeout_allowed>
</command>

<active-response>
<disabled>no</disabled>
<command>windows_firewall</command>
<location>local</location>
<timeout>60</timeout>
</active-response>
```

#. Restart the Wazuh manager to apply the changes:

```console
systemctl restart wazuh-manager
```

With this configuration, Wazuh runs an executable instead of a Python script when triggering an active response on a Windows endpoint.
118 changes: 118 additions & 0 deletions backend/app/active_response/routes/active_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import json
from pathlib import Path

import aiofiles
from fastapi import APIRouter
from fastapi import HTTPException
from fastapi import Security
from fastapi.responses import JSONResponse
from loguru import logger

from app.active_response.schema.active_response import ActiveResponse
from app.active_response.schema.active_response import ActiveResponseDetails
from app.active_response.schema.active_response import ActiveResponsesSupported
from app.active_response.schema.active_response import ActiveResponsesSupportedResponse
from app.active_response.schema.active_response import InvokeActiveResponseRequest
from app.active_response.schema.active_response import InvokeActiveResponseResponse
from app.auth.utils import AuthHandler
from app.connectors.wazuh_manager.utils.universal import send_put_request

active_response_router = APIRouter()


async def verify_active_response_name(active_response_name: str) -> None:
"""
Verify the active response name
"""
active_response_name = active_response_name.upper()
if active_response_name not in ActiveResponsesSupported.__members__:
raise HTTPException(status_code=404, detail="Active Response not found")


def get_markdown_content_path(directory: str, filename: str) -> str:
"""
Get the path to the markdown content
"""
current_directory = Path(__file__).parent.parent
return str(current_directory / f"scripts/{directory}/{filename}")


async def read_markdown_file(file_path: str) -> str:
"""
Read the content of a markdown file
"""
async with aiofiles.open(file_path, "r") as file:
return await file.read()


@active_response_router.get(
"/describe/{active_response_name}",
response_model=ActiveResponse,
description="Get the details of a specific active response",
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
)
async def get_active_response_details_route(active_response_name: str) -> ActiveResponse:
"""
Get the details of a specific active response
"""
await verify_active_response_name(active_response_name)
directory = active_response_name.split("_")[0].lower()
file_path = get_markdown_content_path(directory, f"{active_response_name}.md")
logger.info(f"Reading markdown file: {file_path}")
response = ActiveResponseDetails(
name=active_response_name,
description=ActiveResponsesSupported[active_response_name.upper()].value,
markdown_content=await read_markdown_file(file_path),
)
return JSONResponse(content=response.dict())


@active_response_router.get(
"/supported",
response_model=ActiveResponsesSupportedResponse,
description="Get the list of supported active responses",
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
)
async def get_supported_active_responses_route() -> ActiveResponsesSupportedResponse:
"""
Get the list of supported active responses
"""
return ActiveResponsesSupportedResponse(
supported_active_responses=[
ActiveResponse(name=active_response.name, description=active_response.value) for active_response in ActiveResponsesSupported
],
success=True,
message="Supported Active Responses retrieved successfully",
)


@active_response_router.post(
"/invoke",
response_model=InvokeActiveResponseResponse,
description="Invoke an active response",
dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
)
async def invoke_active_response_route(
request: InvokeActiveResponseRequest,
) -> InvokeActiveResponseResponse:
"""
Invoke an active response.
Args:
request (InvokeActiveResponseRequest): The request object containing the command, custom, arguments, and alert.
Returns:
InvokeActiveResponseResponse: The response object indicating the success or failure of the active response invocation.
"""
logger.info("Invoking Wazuh Active Response...")
# Append '0' to the command - This is required for Wazuh Active Response
request.command = f"{request.command.value}0"
# Create a dictionary with the request data
data_dict = {"command": request.command, "custom": request.custom, "arguments": request.arguments, "alert": request.alert}
await send_put_request(
endpoint=request.endpoint,
data=json.dumps(data_dict),
params=request.params,
)

return InvokeActiveResponseResponse(success=True, message="Wazuh Active Response invoked successfully")
107 changes: 107 additions & 0 deletions backend/app/active_response/schema/active_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from enum import Enum
from typing import Any
from typing import Dict
from typing import List

from fastapi import HTTPException
from pydantic import BaseModel
from pydantic import Field
from pydantic import root_validator


class ActiveResponsesSupported(Enum):
WINDOWS_FIREWALL = "Block or unblock any outbound traffic to the defined IP address via the Windows Firewall"
# Add more active responses here as needed


class ActiveResponse(BaseModel):
name: str
description: str


class ActiveResponsesSupportedResponse(BaseModel):
supported_active_responses: List[ActiveResponse]
success: bool
message: str


class ActiveResponseDetails(BaseModel):
name: str
description: str
markdown_content: str

class Config:
json_encoders = {str: lambda v: v.encode("utf-8", "ignore").decode("utf-8")}


# ! Invoke Active Response ! #
class AlertAction(str, Enum):
unblock = "unblock"
block = "block"


class BaseModelWithEnum(BaseModel):
class Config:
use_enum_values = True


class WindowsFirewallAlert(BaseModelWithEnum):
action: AlertAction
ip: str


class LinuxFirewallAlert(BaseModelWithEnum):
action: AlertAction
ip: str


class ActiveResponseCommand(str, Enum):
windows_firewall = "windows_firewall"
linux_firewall = "linux_firewall"

@classmethod
def _missing_(cls, value):
for member in cls:
if member.name == value:
return member

for active_response in ActiveResponsesSupported:
if active_response.name.lower() == value.lower():
return cls[f"{value}0"]

raise HTTPException(
status_code=400,
detail=f"Invalid command: {value}, must be one of {', '.join([member.name for member in cls])}",
)


class ParamsModel(BaseModel):
wait_for_complete: bool
agents_list: List[str]


class InvokeActiveResponseRequest(BaseModel):
endpoint: str = Field("active-response", const=True)
arguments: list[str] = Field(default_factory=list)
command: ActiveResponseCommand
custom: bool = Field(True, const=True)
alert: Dict[str, Any]
params: ParamsModel

@root_validator(pre=True)
def create_alert(cls, values):
command = values.get("command")
alert = values.get("alert")
if command == ActiveResponseCommand.windows_firewall:
values["alert"] = WindowsFirewallAlert(**alert)
elif command == ActiveResponseCommand.linux_firewall:
values["alert"] = LinuxFirewallAlert(**alert)
else:
raise HTTPException(status_code=400, detail="Invalid command for alert")

return values


class InvokeActiveResponseResponse(BaseModel):
success: bool
message: str
Loading

0 comments on commit dc5fda1

Please sign in to comment.