Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions pyoaev/configuration/settings_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,13 @@ class ConfigLoaderCollector(BaseConfigModel):
description="Optional author override for the connector's payloads and contracts. "
"When absent, the platform attributes them to the connector's name.",
)
platform_description: str | None = Field(
default=None,
description="Description applied to the security platform auto-created by the "
"collector, so its read-only card is not empty in the platform UI.",
)
platform_tags: list[str] | None = Field(
default=None,
description="Tag names applied to the security platform auto-created by the "
"collector.",
)
Comment thread
SamuelHassine marked this conversation as resolved.
Outdated
66 changes: 55 additions & 11 deletions pyoaev/daemons/collector_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

DEFAULT_PERIOD_SECONDS = 60

# Neutral grey used for tags auto-created alongside the security platform,
# consistent with the tag colors already used across collectors.
DEFAULT_PLATFORM_TAG_COLOR = "#6b7280"


class CollectorDaemon(BaseDaemon):
"""Implementation of a daemon of Collector type. Note that it requires
Expand All @@ -19,6 +23,11 @@ class CollectorDaemon(BaseDaemon):
`collector_author` (optional): source-declared author override for the
collector's payloads and contracts; when absent, the platform attributes
them to the collector's name.
`collector_platform_description` (optional): description applied to the
security platform auto-created when `collector_platform` is set, so the
read-only card is not empty in the UI.
`collector_platform_tags` (optional): list of tag names (or a
comma-separated string) applied to the auto-created security platform.
"""

def __init__(
Expand All @@ -43,18 +52,25 @@ def _setup(self):
collector_icon = (icon_name, icon_file_handle, "image/png")
document = self.api.document.upsert(document={}, file=collector_icon)
if self._configuration.get("collector_platform") is not None:
security_platform_input = {
"asset_name": self._configuration.get("collector_name"),
"asset_external_reference": self._configuration.get("collector_id"),
"security_platform_type": self._configuration.get(
"collector_platform"
),
"security_platform_logo_light": document.get("document_id"),
"security_platform_logo_dark": document.get("document_id"),
}
description = self._configuration.get("collector_platform_description")
if description:
security_platform_input["asset_description"] = description
tag_ids = self.__upsert_platform_tags(
self._configuration.get("collector_platform_tags")
)
if tag_ids:
security_platform_input["asset_tags"] = tag_ids
security_platform = self.api.security_platform.upsert(
{
"asset_name": self._configuration.get("collector_name"),
"asset_external_reference": self._configuration.get(
"collector_id"
),
"security_platform_type": self._configuration.get(
"collector_platform"
),
"security_platform_logo_light": document.get("document_id"),
"security_platform_logo_dark": document.get("document_id"),
}
security_platform_input
)
else:
security_platform = {}
Expand All @@ -75,6 +91,34 @@ def _setup(self):

PingAlive(self.api, config, self.logger, "collector").start()

def __upsert_platform_tags(self, tag_names) -> list:
"""Upserts the configured security platform tags and returns their ids.

Accepts either a list of tag names or a comma-separated string; tag
upsert failures are logged and skipped so the collector setup never
fails because of a tag.
"""
if not tag_names:
return []
if isinstance(tag_names, str):
tag_names = [name.strip() for name in tag_names.split(",")]
tag_ids = []
for tag_name in tag_names:
if not tag_name:
continue
Comment thread
SamuelHassine marked this conversation as resolved.
try:
tag = self.api.tag.upsert(
{"tag_name": tag_name, "tag_color": DEFAULT_PLATFORM_TAG_COLOR}
)
tag_id = tag.get("tag_id")
if tag_id:
tag_ids.append(tag_id)
except Exception as err: # noqa: BLE001
self.logger.warning(
f"Could not upsert security platform tag {tag_name}: {err}"
)
return tag_ids

def _start_loop(self):
scheduler = sched.scheduler(time.time, time.sleep)
delay = self._configuration.get("collector_period")
Expand Down
116 changes: 116 additions & 0 deletions test/daemons/test_collector_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,122 @@ def test_when_no_period_config_provided_set_default_period(

self.assertEqual(config.get("collector_period"), DEFAULT_PERIOD_SECONDS)

@patch("pyoaev.apis.SecurityPlatformManager.upsert")
@patch("pyoaev.apis.TagManager.upsert")
@patch("pyoaev.apis.DocumentManager.upsert")
@patch("pyoaev.apis.CollectorManager.create")
@patch("builtins.open", new_callable=mock_open, read_data="data")
Comment on lines +40 to +44
@patch("pyoaev.utils.PingAlive.start")
def test_security_platform_upsert_carries_description_and_tags(
self,
mock_ping_alive,
mock_open_local,
mock_collector_create,
mock_document_upsert,
mock_tag_upsert,
mock_security_platform_upsert,
):
mock_ping_alive.return_value = None
mock_collector_create.return_value = {}
mock_document_upsert.return_value = {"document_id": "doc id"}
mock_tag_upsert.side_effect = [{"tag_id": "tag-1"}, {"tag_id": "tag-2"}]
mock_security_platform_upsert.return_value = {"asset_id": "sp id"}
config = Configuration(
config_hints={
"openaev_url": {"data": "fake"},
"openaev_token": {"data": "fake"},
"collector_id": {"data": "fake id"},
"collector_name": {"data": "Fake EDR"},
"collector_platform": {"data": "EDR"},
"collector_platform_description": {"data": "A fake EDR platform."},
"collector_platform_tags": {"data": ["edr", "fake-vendor"]},
}
)
collector = CollectorDaemon(configuration=config, collector_type="test")

collector._setup()

payload = mock_security_platform_upsert.call_args.args[0]
self.assertEqual(payload["asset_description"], "A fake EDR platform.")
self.assertEqual(payload["asset_tags"], ["tag-1", "tag-2"])
self.assertEqual(mock_tag_upsert.call_count, 2)
self.assertEqual(mock_tag_upsert.call_args_list[0].args[0]["tag_name"], "edr")

Comment thread
SamuelHassine marked this conversation as resolved.
@patch("pyoaev.apis.SecurityPlatformManager.upsert")
@patch("pyoaev.apis.TagManager.upsert")
@patch("pyoaev.apis.DocumentManager.upsert")
@patch("pyoaev.apis.CollectorManager.create")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("pyoaev.utils.PingAlive.start")
def test_security_platform_tags_accept_comma_separated_string(
self,
mock_ping_alive,
mock_open_local,
mock_collector_create,
mock_document_upsert,
mock_tag_upsert,
mock_security_platform_upsert,
):
mock_ping_alive.return_value = None
mock_collector_create.return_value = {}
mock_document_upsert.return_value = {"document_id": "doc id"}
mock_tag_upsert.side_effect = [{"tag_id": "tag-1"}, {"tag_id": "tag-2"}]
mock_security_platform_upsert.return_value = {"asset_id": "sp id"}
config = Configuration(
config_hints={
"openaev_url": {"data": "fake"},
"openaev_token": {"data": "fake"},
"collector_id": {"data": "fake id"},
"collector_name": {"data": "Fake SIEM"},
"collector_platform": {"data": "SIEM"},
"collector_platform_tags": {"data": "siem, fake-vendor"},
}
)
collector = CollectorDaemon(configuration=config, collector_type="test")

collector._setup()

payload = mock_security_platform_upsert.call_args.args[0]
self.assertNotIn("asset_description", payload)
self.assertEqual(payload["asset_tags"], ["tag-1", "tag-2"])

@patch("pyoaev.apis.SecurityPlatformManager.upsert")
@patch("pyoaev.apis.TagManager.upsert")
@patch("pyoaev.apis.DocumentManager.upsert")
@patch("pyoaev.apis.CollectorManager.create")
@patch("builtins.open", new_callable=mock_open, read_data="data")
@patch("pyoaev.utils.PingAlive.start")
def test_security_platform_upsert_without_description_and_tags_is_unchanged(
self,
mock_ping_alive,
mock_open_local,
mock_collector_create,
mock_document_upsert,
mock_tag_upsert,
mock_security_platform_upsert,
):
mock_ping_alive.return_value = None
mock_collector_create.return_value = {}
mock_document_upsert.return_value = {"document_id": "doc id"}
mock_security_platform_upsert.return_value = {"asset_id": "sp id"}
config = Configuration(
config_hints={
"openaev_url": {"data": "fake"},
"openaev_token": {"data": "fake"},
"collector_id": {"data": "fake id"},
"collector_name": {"data": "Fake XDR"},
"collector_platform": {"data": "XDR"},
}
)
collector = CollectorDaemon(configuration=config, collector_type="test")

collector._setup()

payload = mock_security_platform_upsert.call_args.args[0]
self.assertNotIn("asset_description", payload)
self.assertNotIn("asset_tags", payload)
mock_tag_upsert.assert_not_called()


if __name__ == "__main__":
unittest.main()
Loading