Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions HolmSecurity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-07-17

### Added

- Holm Security module with API token authentication
- Account validator
- Device asset connector that collects agent-managed devices from `GET /v2/devices` and maps them to the OCSF Device Inventory Info model
16 changes: 16 additions & 0 deletions HolmSecurity/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.11

WORKDIR /app

RUN pip install poetry

# Install dependencies
COPY poetry.lock pyproject.toml /app/
RUN poetry config virtualenvs.create false && poetry install --only main

COPY . .

RUN useradd -ms /bin/bash sekoiaio-runtime
USER sekoiaio-runtime

ENTRYPOINT [ "python", "./main.py" ]
37 changes: 37 additions & 0 deletions HolmSecurity/connector_holm_security_device_assets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "Holm Security Devices",
"description": "Fetch agent-managed devices enrolled in Holm Security as OCSF device assets",
"uuid": "4df342f5-9fb0-4d45-97c0-bdae48e2e7c2",
"docker_parameters": "holm_security_device_asset_connector",
"type": "asset",
"capability": "host_detection",
"arguments": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"sekoia_base_url": {
"description": "Sekoia base URL",
"type": "string"
},
"frequency": {
"type": "integer",
"description": "Batch frequency in seconds",
"minimum": 1,
"default": 60
},
"sekoia_api_key": {
"description": "API key to use from sekoia.io",
"type": "string"
},
"batch_size": {
"type": "integer",
"description": "Number of assets to process in one batch",
"minimum": 1,
"default": 100
}
},
"required": ["sekoia_api_key"],
"secrets": ["sekoia_api_key"],
"internal": ["sekoia_base_url", "sekoia_api_key", "frequency", "batch_size"]
}
}
Empty file.
82 changes: 82 additions & 0 deletions HolmSecurity/holm_security/account_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from functools import cached_property

from requests import Response
from requests.exceptions import RequestException, Timeout
from sekoia_automation.account_validator import AccountValidator

from holm_security.client import ApiClient


class HolmSecurityAccountValidator(AccountValidator):
"""Validate Holm Security credentials by pinging the devices endpoint.

A ``200 OK`` on ``GET /v2/devices?page_size=1`` confirms the token is valid.
"""

TIMEOUT = 30
VALIDATION_ENDPOINT = "/v2/devices"

@cached_property
def base_url(self) -> str:
return str(self.module.configuration["base_url"]).rstrip("/")

@cached_property
def client(self) -> ApiClient:
return ApiClient(base_url=self.base_url, token=self.module.configuration["api_token"])

@cached_property
def validation_url(self) -> str:
return f"{self.base_url}{self.VALIDATION_ENDPOINT}"

@staticmethod
def _describe_error(response: Response) -> str:
code = response.status_code

try:
detail = response.json()
except ValueError:
detail = response.text

if 400 <= code < 500:
return f"Client error ({code}) while validating Holm Security credentials: {detail}"

if 500 <= code < 600:
return f"Server error ({code}) while validating Holm Security credentials: {detail}"
return f"Unexpected status ({code}) while validating Holm Security credentials: {detail}"

def validate(self) -> bool:
self.log(message="Starting credentials validation for Holm Security asset connector", level="info")

try:
response = self.client.get(self.validation_url, params={"page_size": 1}, timeout=self.TIMEOUT)
except Timeout:
message = f"Timeout while validating Holm Security credentials at {self.validation_url}"
self.log(message=message, level="error")
self.error(message)

return False

except RequestException as error:
message = f"Network error while validating Holm Security credentials: {error}"
self.log(message=message, level="error")
self.error(message)

return False

except Exception as error:
message = f"Unexpected error while validating Holm Security credentials: {error}"
self.log(message=message, level="error")
self.error(message)

return False

if response.status_code == 200:
self.log(message="Holm Security credentials validated successfully", level="info")
return True

message = self._describe_error(response)

self.log(message=message, level="error")
self.error(message)

return False
Empty file.
Loading