Skip to content

Feature: Holm Security Vulnerabilities#2915

Open
vg-svitla wants to merge 1 commit into
feature/holm_security_devicesfrom
feature/holm_security_vulnerabilities
Open

Feature: Holm Security Vulnerabilities#2915
vg-svitla wants to merge 1 commit into
feature/holm_security_devicesfrom
feature/holm_security_vulnerabilities

Conversation

@vg-svitla

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

Copy link
Copy Markdown
Collaborator

Relates to 1620, 1517

Summary by Sourcery

Introduce a Holm Security vulnerability asset connector that maps vulnerability findings to OCSF and aligns device mapping via shared helpers, while updating account validation and module wiring accordingly.

New Features:

  • Add a Holm Security vulnerability asset connector that ingests vulnerability findings, correlates them with managed devices, and emits OCSF Vulnerability Finding events.
  • Provide vulnerability-specific pydantic models and a YAML mapping describing how Holm Security vulnerability findings are transformed into OCSF events.
  • Register the new vulnerability asset connector in the module entrypoint and add its connector manifest.

Enhancements:

  • Extract shared device mapping utilities into a reusable mappers module used by both device and vulnerability connectors.
  • Refine Holm Security account validation to check both device and vulnerability endpoints and improve error messages with endpoint context.

Build:

  • Bump the Holm Security integration version from 1.0.0 to 1.1.0.

Documentation:

  • Update the Holm Security changelog to document the new vulnerability connector and account validator changes.

Tests:

  • Add unit tests for the Holm Security vulnerability asset connector covering mapping logic, pagination, checkpointing, and error handling.
  • Extend account validator tests to cover multi-endpoint validation and parameter usage.

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

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce a Holm Security vulnerability asset connector that maps vulnerability findings to OCSF, refactor shared device mapping logic into reusable helpers, and tighten account validation by requiring both device and vulnerability endpoints to succeed, with comprehensive tests and manifest/changelog updates.

Sequence diagram for tightened Holm Security account validation

sequenceDiagram
    actor User
    participant HolmSecurityAccountValidator as HolmSecurityAccountValidator
    participant ApiClient as ApiClient
    participant HolmSecurityAPI as HolmSecurityAPI

    User->>HolmSecurityAccountValidator: validate()
    HolmSecurityAccountValidator->>HolmSecurityAccountValidator: log("Starting credentials validation", info)

    loop VALIDATION_ENDPOINTS
        HolmSecurityAccountValidator->>HolmSecurityAccountValidator: _check_endpoint(endpoint, params)
        HolmSecurityAccountValidator->>ApiClient: client.get(base_url + endpoint, params, timeout)
        alt success
            ApiClient-->>HolmSecurityAccountValidator: Response(status_code=200)
            HolmSecurityAccountValidator-->>HolmSecurityAccountValidator: return True
        else non_200
            ApiClient-->>HolmSecurityAccountValidator: Response(status_code!=200)
            HolmSecurityAccountValidator->>HolmSecurityAccountValidator: _describe_error(url, response)
            HolmSecurityAccountValidator->>HolmSecurityAccountValidator: log(error_message, error)
            HolmSecurityAccountValidator->>HolmSecurityAccountValidator: error(error_message)
            HolmSecurityAccountValidator-->>HolmSecurityAccountValidator: return False
        end
    end

    alt all endpoints succeeded
        HolmSecurityAccountValidator->>HolmSecurityAccountValidator: log("credentials validated successfully", info)
        HolmSecurityAccountValidator-->>User: True
    else some endpoint failed
        HolmSecurityAccountValidator-->>User: False
    end
Loading

File-Level Changes

Change Details Files
Refactor Holm device-to-OCSF mapping helpers into a shared module and update the device asset connector to use them.
  • Extract timestamp, device type, OS, and network interface mapping logic from the device connector into holm_security.asset_connector.mappers.
  • Simplify HolmSecurityDeviceAssetConnector._to_epoch, build_device_type, build_operating_system, and build_network_interfaces to delegate to the shared mappers.
  • Remove now-redundant OS_FAMILY_MAP and related type imports from the device asset connector.
HolmSecurity/holm_security/asset_connector/device_assets.py
HolmSecurity/holm_security/asset_connector/mappers.py
Strengthen Holm Security account validation by checking both devices and vulnerability report endpoints, and update error messaging and tests accordingly.
  • Replace single VALIDATION_ENDPOINT with a list of endpoint+params pairs (devices and vulnerabilities) that must each return 200.
  • Factor request/response handling into _check_endpoint, including endpoint-specific timeout, network, and unexpected error messages.
  • Adjust _describe_error to include the URL, and update validate to iterate endpoints and log a single success on full pass.
  • Expand tests to cover dual-endpoint success, per-endpoint failures, and updated query parameter assertions.
HolmSecurity/holm_security/account_validator.py
HolmSecurity/tests/test_account_validator.py
Add a vulnerability asset connector that collects Holm vulnerability findings, correlates them with managed devices, maps them to OCSF Vulnerability Finding events, and persists a detection-time checkpoint.
  • Implement HolmSecurityVulnerabilityAssetConnector with pagination helpers, device cache building from /v2/devices, and correlation of findings hosts to managed devices or minimal unmanaged devices.
  • Map Holm severity and status codes to OCSF activity/severity fields and build VulnerabilityOCSFModel instances with metadata, finding_info, vulnerabilities, and device sub-objects.
  • Track most recent last_detected across runs via PersistentJSON context and update checkpoints after generation, with logging of generated/skipped counts and API failures.
  • Wire the new connector into main.py registration and add an empty connector_holm_security_vulnerability_assets.json placeholder for the platform.
HolmSecurity/holm_security/asset_connector/vulnerability_assets.py
HolmSecurity/holm_security/asset_connector/vulnerability_models.py
HolmSecurity/main.py
HolmSecurity/connector_holm_security_vulnerability_assets.json
Provide declarative mapping documentation and comprehensive unit tests for the vulnerability connector behavior.
  • Introduce vulnerability_mapping.yml describing source-to-OCSF field mappings, logic, and sample payloads for Holm vulnerability findings.
  • Add tests that cover severity/status mapping, CVE and non-CVE vulnerability details, device correlation via hostname/IP, checkpoint filtering and pagination, skipping invalid findings, and API error propagation.
  • Use factory helpers in tests to build representative HolmDevice and HolmVulnerabilityFinding payloads, and validate the resulting VulnerabilityOCSFModel structure and caching behavior.
HolmSecurity/holm_security/asset_connector/vulnerability_mapping.yml
HolmSecurity/tests/asset_connector/test_vulnerability_assets.py
Update manifest and changelog to reflect the new vulnerability connector release.
  • Bump manifest.json version from 1.0.0 to 1.1.0.
  • Add CHANGELOG entry 1.1.0 detailing the introduction of the vulnerability asset connector and account validator changes.
HolmSecurity/manifest.json
HolmSecurity/CHANGELOG.md

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

@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 3 issues, and left some high level feedback:

  • In get_assets, the direct use of isoparse on finding.last_detected and the checkpoint value can cause the whole run to fail on a single malformed timestamp; consider using a safer parser (similar to mappers.to_epoch) with error handling so bad records are skipped rather than aborting the connector.
  • The _fetch_pages helper assumes the Holm API always returns a well-formed JSON structure for pagination and immediately calls response.json() and model_validate; adding small defensive checks or logging around unexpected payload shapes (e.g. missing next or results) would make failures easier to diagnose without relying solely on pydantic validation errors.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_assets`, the direct use of `isoparse` on `finding.last_detected` and the checkpoint value can cause the whole run to fail on a single malformed timestamp; consider using a safer parser (similar to `mappers.to_epoch`) with error handling so bad records are skipped rather than aborting the connector.
- The `_fetch_pages` helper assumes the Holm API always returns a well-formed JSON structure for pagination and immediately calls `response.json()` and `model_validate`; adding small defensive checks or logging around unexpected payload shapes (e.g. missing `next` or `results`) would make failures easier to diagnose without relying solely on pydantic validation errors.

## Individual Comments

### Comment 1
<location path="HolmSecurity/holm_security/asset_connector/vulnerability_assets.py" line_range="355-361" />
<code_context>
+            level="info",
+        )
+
+    def update_checkpoint(self) -> None:
+        if self._latest_time:
+            with self.context as cache:
+                cache["most_recent_last_detected"] = self._latest_time
+            self.log(message=f"Checkpoint updated to {self._latest_time}", level="debug")
+        else:
+            self.log(message="No checkpoint update needed", level="debug")
</code_context>
<issue_to_address>
**issue (bug_risk):** Initialize `_latest_time` to avoid AttributeError in `update_checkpoint` when no findings are processed.

`update_checkpoint` assumes `self._latest_time` exists, but it’s only set in `get_assets` when `max_raw` changes. If there are no findings or all are skipped before `_latest_time` is set, `update_checkpoint` will raise `AttributeError`. Initialize `self._latest_time: str | None = None` in `__init__`, or use `getattr(self, "_latest_time", None)` inside `update_checkpoint` to avoid this failure.
</issue_to_address>

### Comment 2
<location path="HolmSecurity/holm_security/asset_connector/vulnerability_assets.py" line_range="324-325" />
<code_context>
+        # Steps 2 & 3: fetch vulnerability findings, correlate, and emit.
+        for page in self._fetch_pages(self.VULNERABILITIES_ENDPOINT, HolmVulnerabilityPage):
+            assert isinstance(page, HolmVulnerabilityPage)
+            for finding in page.results:
+                finding_dt = isoparse(finding.last_detected) if finding.last_detected else None
+
+                # Client-side checkpoint filter: skip findings not detected since last run.
</code_context>
<issue_to_address>
**issue (bug_risk):** Use a safer timestamp parsing strategy for `last_detected` to avoid aborting the run on malformed data.

In `get_assets`, `finding_dt` is derived via `isoparse(finding.last_detected)`. If Holm returns a malformed timestamp, this will raise and stop the whole connector run. Consider either reusing `mappers.to_epoch` (then converting to `datetime`) or wrapping `isoparse` in try/except and treating unparsable values as `None` so a single bad record doesn’t break ingestion.
</issue_to_address>

### Comment 3
<location path="HolmSecurity/holm_security/asset_connector/vulnerability_assets.py" line_range="165-179" />
<code_context>
+    def build_device_from_cache(self, cached: HolmDevice, host: HolmVulnHost | None) -> Device:
+        """Build a full OCSF Device from a cached managed device."""
+        device_type, device_type_id = mappers.map_device_type(cached.os_is_server)
+        network = cached.network
+        host_name = host.name if host else None
+        host_ip = host.ip if host else None
+
+        return Device(
+            uid=cached.uid,
+            type=device_type,
+            type_id=device_type_id,
+            hostname=cached.hostname or host_name or "",
+            name=cached.device_name or host_name,
+            ip=(network.ip_address if network else None) or host_ip,
+            os=mappers.build_operating_system(cached.os_name, cached.os_family),
+            network_interfaces=mappers.build_network_interfaces(network, cached.hostname),
+            first_seen_time=mappers.to_epoch(cached.created),
+            last_seen_time=mappers.to_epoch(cached.last_sync),
</code_context>
<issue_to_address>
**suggestion:** Prefer the host name from the finding when populating network interface hostname.

In `build_device_from_cache`, `network_interfaces` are built with `cached.hostname`, even when `host_name` is available. If the cached device has no hostname but the finding does, interface hostnames are lost. Use `host_name` (computed earlier) instead of `cached.hostname` when calling `build_network_interfaces` to keep interface hostnames aligned with the resolved device fields.

```suggestion
        network = cached.network
        host_name = host.name if host else None
        host_ip = host.ip if host else None

        return Device(
            uid=cached.uid,
            type=device_type,
            type_id=device_type_id,
            hostname=cached.hostname or host_name or "",
            name=cached.device_name or host_name,
            ip=(network.ip_address if network else None) or host_ip,
            os=mappers.build_operating_system(cached.os_name, cached.os_family),
            network_interfaces=mappers.build_network_interfaces(network, host_name or cached.hostname),
            first_seen_time=mappers.to_epoch(cached.created),
            last_seen_time=mappers.to_epoch(cached.last_sync),
```
</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 +355 to +361
def update_checkpoint(self) -> None:
if self._latest_time:
with self.context as cache:
cache["most_recent_last_detected"] = self._latest_time
self.log(message=f"Checkpoint updated to {self._latest_time}", level="debug")
else:
self.log(message="No checkpoint update needed", level="debug")

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.

issue (bug_risk): Initialize _latest_time to avoid AttributeError in update_checkpoint when no findings are processed.

update_checkpoint assumes self._latest_time exists, but it’s only set in get_assets when max_raw changes. If there are no findings or all are skipped before _latest_time is set, update_checkpoint will raise AttributeError. Initialize self._latest_time: str | None = None in __init__, or use getattr(self, "_latest_time", None) inside update_checkpoint to avoid this failure.

Comment on lines +324 to +325
for finding in page.results:
finding_dt = isoparse(finding.last_detected) if finding.last_detected else None

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.

issue (bug_risk): Use a safer timestamp parsing strategy for last_detected to avoid aborting the run on malformed data.

In get_assets, finding_dt is derived via isoparse(finding.last_detected). If Holm returns a malformed timestamp, this will raise and stop the whole connector run. Consider either reusing mappers.to_epoch (then converting to datetime) or wrapping isoparse in try/except and treating unparsable values as None so a single bad record doesn’t break ingestion.

Comment on lines +165 to +179
network = cached.network
host_name = host.name if host else None
host_ip = host.ip if host else None

return Device(
uid=cached.uid,
type=device_type,
type_id=device_type_id,
hostname=cached.hostname or host_name or "",
name=cached.device_name or host_name,
ip=(network.ip_address if network else None) or host_ip,
os=mappers.build_operating_system(cached.os_name, cached.os_family),
network_interfaces=mappers.build_network_interfaces(network, cached.hostname),
first_seen_time=mappers.to_epoch(cached.created),
last_seen_time=mappers.to_epoch(cached.last_sync),

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: Prefer the host name from the finding when populating network interface hostname.

In build_device_from_cache, network_interfaces are built with cached.hostname, even when host_name is available. If the cached device has no hostname but the finding does, interface hostnames are lost. Use host_name (computed earlier) instead of cached.hostname when calling build_network_interfaces to keep interface hostnames aligned with the resolved device fields.

Suggested change
network = cached.network
host_name = host.name if host else None
host_ip = host.ip if host else None
return Device(
uid=cached.uid,
type=device_type,
type_id=device_type_id,
hostname=cached.hostname or host_name or "",
name=cached.device_name or host_name,
ip=(network.ip_address if network else None) or host_ip,
os=mappers.build_operating_system(cached.os_name, cached.os_family),
network_interfaces=mappers.build_network_interfaces(network, cached.hostname),
first_seen_time=mappers.to_epoch(cached.created),
last_seen_time=mappers.to_epoch(cached.last_sync),
network = cached.network
host_name = host.name if host else None
host_ip = host.ip if host else None
return Device(
uid=cached.uid,
type=device_type,
type_id=device_type_id,
hostname=cached.hostname or host_name or "",
name=cached.device_name or host_name,
ip=(network.ip_address if network else None) or host_ip,
os=mappers.build_operating_system(cached.os_name, cached.os_family),
network_interfaces=mappers.build_network_interfaces(network, host_name or cached.hostname),
first_seen_time=mappers.to_epoch(cached.created),
last_seen_time=mappers.to_epoch(cached.last_sync),

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