Skip to content

Feature: Holm Security Devices#2914

Open
vg-svitla wants to merge 3 commits into
developfrom
feature/holm_security_devices
Open

Feature: Holm Security Devices#2914
vg-svitla wants to merge 3 commits into
developfrom
feature/holm_security_devices

Conversation

@vg-svitla

@vg-svitla vg-svitla commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Relates to 1774, 1772

Summary by Sourcery

Add a Holm Security automation module that authenticates against the Holm Security API, validates credentials, and exposes a device asset connector producing OCSF Device Inventory Info events from /v2/devices.

New Features:

  • Introduce a Holm Security device asset connector that collects agent-managed devices and maps them into OCSF Device Inventory Info assets.
  • Provide an account validator that checks Holm Security credentials by calling the devices endpoint.
  • Add a dedicated Holm Security API client with token-based authentication and retry handling.
  • Define Pydantic models and field-mapping configuration for Holm Security device records to OCSF Device Inventory Info.
  • Register the Holm Security module, connector, and validator entrypoint for automation runtime.
  • Expose module configuration and metadata via a manifest for Holm Security integration.

Enhancements:

  • Set up project tooling for the Holm Security module, including pyproject configuration, formatting, type-checking, and coverage settings.

Build:

  • Configure Python packaging and dependencies for the Holm Security automation module via pyproject.toml.

Documentation:

  • Add initial changelog documenting the introduction of the Holm Security module, account validator, and device asset connector.

Tests:

  • Add unit tests for the Holm Security device asset connector covering OCSF mapping, pagination, checkpointing, and error handling.
  • Add unit tests for the Holm Security account validator covering success, API failures, timeouts, and unexpected errors.
  • Add shared test fixtures for temporary storage used by the connector.

@vg-svitla
vg-svitla requested a review from TOUFIKIzakarya July 22, 2026 14:01
@sourcery-ai

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce a Holm Security integration module that authenticates via API token, validates credentials, and exposes a device asset connector that reads /v2/devices, maps Holm device records into OCSF Device Inventory Info events, and persists a checkpoint for incremental collection, with tests and project configuration.

Sequence diagram for Holm Security device asset collection

sequenceDiagram
    actor Operator
    participant Module
    participant HolmSecurityDeviceAssetConnector as DeviceConnector
    participant PersistentJSON as CheckpointStore
    participant ApiClient
    participant HolmSecurityAPI as HolmSecurity_API

    Operator->>Module: run()
    Module->>DeviceConnector: get_assets()

    DeviceConnector->>CheckpointStore: read most_recent_last_sync
    CheckpointStore-->>DeviceConnector: checkpoint

    loop fetch device pages
        DeviceConnector->>ApiClient: get(base_url + /v2/devices, page_size)
        ApiClient->>HolmSecurity_API: GET /v2/devices
        HolmSecurity_API-->>ApiClient: 200 JSON page
        ApiClient-->>DeviceConnector: Response
        DeviceConnector->>DeviceConnector: HolmDevicePage.model_validate(json)

        loop for each HolmDevice in page.results
            DeviceConnector->>DeviceConnector: [last_sync <= checkpoint?]
            alt last_sync <= checkpoint
                DeviceConnector->>DeviceConnector: skip device
            else last_sync > checkpoint or no checkpoint
                DeviceConnector->>DeviceConnector: map_fields(device)
                DeviceConnector-->>Module: yield DeviceOCSFModel
            end
        end
    end

    DeviceConnector->>CheckpointStore: write most_recent_last_sync
    DeviceConnector-->>Module: Asset generation complete
Loading

File-Level Changes

Change Details Files
Add Holm Security device asset connector that pulls devices from the Holm Security API and maps them to OCSF Device Inventory Info assets with checkpointing.
  • Implement HolmSecurityDeviceAssetConnector extending AssetConnector with API client configuration and metadata constants for OCSF Device Inventory Info events.
  • Map Holm device fields to OCSF Device and DeviceOCSFModel, including OS type mapping, network interfaces construction, timestamps conversion, and device type resolution.
  • Implement paginated fetching of /v2/devices with client-side checkpoint filtering based on last_sync and persistence of most_recent_last_sync via PersistentJSON context file.
  • Handle malformed or incomplete device records by skipping them with logging, and raise on API RequestException with error logging.
  • Provide a YAML mapping specification documenting source-to-target field mappings and example Holm and OCSF samples for the device inventory connector.
HolmSecurity/holm_security/asset_connector/device_assets.py
HolmSecurity/holm_security/asset_connector/device_mapping.yml
Add Holm Security account validator that verifies credentials by calling the devices endpoint and classifying errors.
  • Implement HolmSecurityAccountValidator extending AccountValidator, building an ApiClient from module configuration and pinging GET /v2/devices?page_size=1 with a 30s timeout.
  • Add helper to describe HTTP error responses, distinguishing client, server, and unexpected status codes and extracting JSON or text body.
  • Implement validate() to log lifecycle, handle Timeout, RequestException, and generic Exception with error reporting, and return True only on 200 OK.
HolmSecurity/holm_security/account_validator.py
Introduce Holm Security API client and pydantic models for Holm device and pagination responses.
  • Implement ApiClient session with bearer token authentication via custom AuthBase, setting Accept header and configuring HTTPAdapter retries for GET on transient status codes.
  • Implement HolmSecurityApiAuthentication that attaches the Holm token using the "Token <api_token>" scheme to the Authorization header.
  • Define HolmNetwork, HolmDevice, and HolmDevicePage pydantic models with extra="allow" to tolerate unknown fields and capture only attributes needed by the OCSF mapping.
HolmSecurity/holm_security/client/__init__.py
HolmSecurity/holm_security/client/auth.py
HolmSecurity/holm_security/asset_connector/models.py
Wire up the Holm Security module entrypoint, manifest, connector configuration, and project tooling.
  • Add manifest.json describing the Holm Security module configuration schema (base_url, api_token), metadata (uuid, slug, version), and supports_validation flag.
  • Register HolmSecurityAccountValidator and HolmSecurityDeviceAssetConnector in main.py and run the module.
  • Add pyproject.toml with Poetry, linting (black, isort), type-checking (mypy), pytest configuration, and dependencies including sekoia-automation-sdk and python-dateutil.
  • Add connector_holm_security_device_assets.json and poetry.lock stubs to integrate with the platform packaging and connector registry.
  • Add CHANGELOG.md documenting initial 1.0.0 release with account validator and device asset connector.
HolmSecurity/manifest.json
HolmSecurity/main.py
HolmSecurity/pyproject.toml
HolmSecurity/connector_holm_security_device_assets.json
HolmSecurity/poetry.lock
HolmSecurity/CHANGELOG.md
Add tests and test utilities for the Holm Security device connector and account validator.
  • Add test_device_assets.py covering device type mapping, OS mapping (including unknown family and absent OS), network interface construction, hostname fallback, timestamp requirements, pagination behavior, checkpoint filtering, checkpoint persistence, skipping invalid devices, and raising on API errors.
  • Add test_account_validator.py validating success path, authentication failure, server error, timeout, connection error, unexpected exceptions, and non-JSON error bodies for the account validator.
  • Add pytest fixture in conftest.py to create and clean up a temporary Symphony storage directory, overriding sekoia_automation.constants.DATA_STORAGE to isolate tests.
HolmSecurity/tests/asset_connector/test_device_assets.py
HolmSecurity/tests/test_account_validator.py
HolmSecurity/tests/conftest.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpypi/​mypy@​2.3.075100100100100
Addedpypi/​types-python-dateutil@​2.9.0.20260716100100100100100

View full report

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • Consider initializing self._latest_time in HolmSecurityDeviceAssetConnector.__init__ to avoid potential AttributeError when update_checkpoint is called before any assets are generated.
  • In HolmSecurityDeviceAssetConnector._to_epoch and the date parsing in get_assets, you may want to handle invalid timestamp formats explicitly to avoid uncaught parsing errors if Holm Security returns unexpected date strings.
  • For the Pydantic models (HolmDevicePage.results and any other list fields), prefer default_factory=list over =[] to avoid shared mutable defaults.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider initializing `self._latest_time` in `HolmSecurityDeviceAssetConnector.__init__` to avoid potential `AttributeError` when `update_checkpoint` is called before any assets are generated.
- In `HolmSecurityDeviceAssetConnector._to_epoch` and the date parsing in `get_assets`, you may want to handle invalid timestamp formats explicitly to avoid uncaught parsing errors if Holm Security returns unexpected date strings.
- For the Pydantic models (`HolmDevicePage.results` and any other list fields), prefer `default_factory=list` over `=[]` to avoid shared mutable defaults.

## Individual Comments

### Comment 1
<location path="HolmSecurity/holm_security/asset_connector/device_assets.py" line_range="221-225" />
<code_context>
+        skipped = 0
+
+        for page in self._fetch_device_pages():
+            for device in page.results:
+                device_dt = isoparse(device.last_sync) if device.last_sync else None
+
+                # Client-side checkpoint filter: skip devices not modified since last run.
</code_context>
<issue_to_address>
**suggestion:** Protect the checkpoint filter from invalid last_sync values to avoid runtime errors.

`isoparse(device.last_sync)` runs outside the existing `try/except`, so a malformed `last_sync` will raise `ValueError` and crash before the error handling is reached. Please wrap the parse in its own `try/except` and treat invalid timestamps as `None`, e.g.:

```python
device_dt: datetime | None = None
if device.last_sync:
    try:
        device_dt = isoparse(device.last_sync)
    except ValueError:
        self.log(message=f"Invalid last_sync on device {device.uid}: {device.last_sync}", level="warning")
```

This keeps the checkpoint filter resilient to bad timestamp data.

```suggestion
        for page in self._fetch_device_pages():
            for device in page.results:
                device_dt: datetime | None = None
                if device.last_sync:
                    try:
                        device_dt = isoparse(device.last_sync)
                    except ValueError:
                        self.log(
                            message=f"Invalid last_sync on device {device.uid}: {device.last_sync}",
                            level="warning",
                        )

                # Client-side checkpoint filter: skip devices not modified since last run.
```
</issue_to_address>

### Comment 2
<location path="HolmSecurity/tests/asset_connector/test_device_assets.py" line_range="104-107" />
<code_context>
+    assert device.os.type_id == OSTypeId.WINDOWS
+
+
+def test_operating_system_unknown_family(connector):
+    device = connector.build_device(HolmDevice.model_validate(make_device(os_family="plan9")))
+    assert device.os.type == OSTypeStr.OTHER
+    assert device.os.type_id == OSTypeId.OTHER
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `build_operating_system` handling of mixed-case and whitespace `os_family` values.

Current tests only cover lowercase `os_family` values and an unknown family, but the implementation normalizes via `strip().lower()`. Please add cases like `os_family=" Windows "` and `"MAC"` to assert they resolve to the correct `OSTypeStr`/`OSTypeId`, so the normalization behavior is explicitly covered.

Suggested implementation:

```python
def test_operating_system_windows(connector):
    device = connector.build_device(HolmDevice.model_validate(make_device()))
    assert device.os is not None
    assert device.os.name == "Microsoft Windows Server 2025 Datacenter"
    assert device.os.type == OSTypeStr.WINDOWS
    assert device.os.type_id == OSTypeId.WINDOWS


def test_operating_system_windows_mixed_case_whitespace(connector):
    device = connector.build_device(
        HolmDevice.model_validate(make_device(os_family=" Windows "))
    )
    assert device.os is not None
    assert device.os.type == OSTypeStr.WINDOWS
    assert device.os.type_id == OSTypeId.WINDOWS


def test_operating_system_mac_mixed_case(connector):
    device = connector.build_device(
        HolmDevice.model_validate(make_device(os_family="MAC"))
    )
    assert device.os is not None
    assert device.os.type == OSTypeStr.MAC_OS
    assert device.os.type_id == OSTypeId.MAC_OS


import pytest

```

1. Verify the exact enum members for macOS in `OSTypeStr` and `OSTypeId` (e.g. they might be `MAC_OS`, `MAC_OS_X`, or similar) and adjust the assertions in `test_operating_system_mac_mixed_case` accordingly.
2. If there is a dedicated factory or fixture for macOS devices (rather than using `make_device(os_family="MAC")`), update the test to use it to stay consistent with the rest of the test suite.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +221 to +225
for page in self._fetch_device_pages():
for device in page.results:
device_dt = isoparse(device.last_sync) if device.last_sync else None

# Client-side checkpoint filter: skip devices not modified since last run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Protect the checkpoint filter from invalid last_sync values to avoid runtime errors.

isoparse(device.last_sync) runs outside the existing try/except, so a malformed last_sync will raise ValueError and crash before the error handling is reached. Please wrap the parse in its own try/except and treat invalid timestamps as None, e.g.:

device_dt: datetime | None = None
if device.last_sync:
    try:
        device_dt = isoparse(device.last_sync)
    except ValueError:
        self.log(message=f"Invalid last_sync on device {device.uid}: {device.last_sync}", level="warning")

This keeps the checkpoint filter resilient to bad timestamp data.

Suggested change
for page in self._fetch_device_pages():
for device in page.results:
device_dt = isoparse(device.last_sync) if device.last_sync else None
# Client-side checkpoint filter: skip devices not modified since last run.
for page in self._fetch_device_pages():
for device in page.results:
device_dt: datetime | None = None
if device.last_sync:
try:
device_dt = isoparse(device.last_sync)
except ValueError:
self.log(
message=f"Invalid last_sync on device {device.uid}: {device.last_sync}",
level="warning",
)
# Client-side checkpoint filter: skip devices not modified since last run.

Comment on lines +104 to +107
def test_operating_system_unknown_family(connector):
device = connector.build_device(HolmDevice.model_validate(make_device(os_family="plan9")))
assert device.os.type == OSTypeStr.OTHER
assert device.os.type_id == OSTypeId.OTHER

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for build_operating_system handling of mixed-case and whitespace os_family values.

Current tests only cover lowercase os_family values and an unknown family, but the implementation normalizes via strip().lower(). Please add cases like os_family=" Windows " and "MAC" to assert they resolve to the correct OSTypeStr/OSTypeId, so the normalization behavior is explicitly covered.

Suggested implementation:

def test_operating_system_windows(connector):
    device = connector.build_device(HolmDevice.model_validate(make_device()))
    assert device.os is not None
    assert device.os.name == "Microsoft Windows Server 2025 Datacenter"
    assert device.os.type == OSTypeStr.WINDOWS
    assert device.os.type_id == OSTypeId.WINDOWS


def test_operating_system_windows_mixed_case_whitespace(connector):
    device = connector.build_device(
        HolmDevice.model_validate(make_device(os_family=" Windows "))
    )
    assert device.os is not None
    assert device.os.type == OSTypeStr.WINDOWS
    assert device.os.type_id == OSTypeId.WINDOWS


def test_operating_system_mac_mixed_case(connector):
    device = connector.build_device(
        HolmDevice.model_validate(make_device(os_family="MAC"))
    )
    assert device.os is not None
    assert device.os.type == OSTypeStr.MAC_OS
    assert device.os.type_id == OSTypeId.MAC_OS


import pytest
  1. Verify the exact enum members for macOS in OSTypeStr and OSTypeId (e.g. they might be MAC_OS, MAC_OS_X, or similar) and adjust the assertions in test_operating_system_mac_mixed_case accordingly.
  2. If there is a dedicated factory or fixture for macOS devices (rather than using make_device(os_family="MAC")), update the test to use it to stay consistent with the rest of the test suite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant