cartography 0.106.0rc1__py3-none-any.whl → 0.107.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cartography might be problematic. Click here for more details.

Files changed (101) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +131 -2
  3. cartography/client/core/tx.py +62 -0
  4. cartography/config.py +42 -0
  5. cartography/driftdetect/cli.py +3 -2
  6. cartography/graph/cleanupbuilder.py +47 -0
  7. cartography/graph/job.py +42 -0
  8. cartography/graph/querybuilder.py +136 -2
  9. cartography/graph/statement.py +1 -1
  10. cartography/intel/airbyte/__init__.py +105 -0
  11. cartography/intel/airbyte/connections.py +120 -0
  12. cartography/intel/airbyte/destinations.py +81 -0
  13. cartography/intel/airbyte/organizations.py +59 -0
  14. cartography/intel/airbyte/sources.py +78 -0
  15. cartography/intel/airbyte/tags.py +64 -0
  16. cartography/intel/airbyte/users.py +106 -0
  17. cartography/intel/airbyte/util.py +122 -0
  18. cartography/intel/airbyte/workspaces.py +63 -0
  19. cartography/intel/aws/__init__.py +1 -0
  20. cartography/intel/aws/cloudtrail_management_events.py +364 -0
  21. cartography/intel/aws/cloudwatch.py +77 -0
  22. cartography/intel/aws/codebuild.py +132 -0
  23. cartography/intel/aws/ec2/subnets.py +1 -1
  24. cartography/intel/aws/ecs.py +17 -0
  25. cartography/intel/aws/efs.py +80 -0
  26. cartography/intel/aws/inspector.py +80 -61
  27. cartography/intel/aws/resources.py +4 -0
  28. cartography/intel/aws/sns.py +62 -2
  29. cartography/intel/entra/users.py +84 -42
  30. cartography/intel/scaleway/__init__.py +127 -0
  31. cartography/intel/scaleway/iam/__init__.py +0 -0
  32. cartography/intel/scaleway/iam/apikeys.py +71 -0
  33. cartography/intel/scaleway/iam/applications.py +71 -0
  34. cartography/intel/scaleway/iam/groups.py +71 -0
  35. cartography/intel/scaleway/iam/users.py +71 -0
  36. cartography/intel/scaleway/instances/__init__.py +0 -0
  37. cartography/intel/scaleway/instances/flexibleips.py +86 -0
  38. cartography/intel/scaleway/instances/instances.py +92 -0
  39. cartography/intel/scaleway/projects.py +79 -0
  40. cartography/intel/scaleway/storage/__init__.py +0 -0
  41. cartography/intel/scaleway/storage/snapshots.py +86 -0
  42. cartography/intel/scaleway/storage/volumes.py +84 -0
  43. cartography/intel/scaleway/utils.py +37 -0
  44. cartography/intel/sentinelone/__init__.py +69 -0
  45. cartography/intel/sentinelone/account.py +140 -0
  46. cartography/intel/sentinelone/agent.py +139 -0
  47. cartography/intel/sentinelone/api.py +113 -0
  48. cartography/intel/sentinelone/application.py +248 -0
  49. cartography/intel/sentinelone/utils.py +28 -0
  50. cartography/models/airbyte/__init__.py +0 -0
  51. cartography/models/airbyte/connection.py +138 -0
  52. cartography/models/airbyte/destination.py +75 -0
  53. cartography/models/airbyte/organization.py +19 -0
  54. cartography/models/airbyte/source.py +75 -0
  55. cartography/models/airbyte/stream.py +74 -0
  56. cartography/models/airbyte/tag.py +69 -0
  57. cartography/models/airbyte/user.py +111 -0
  58. cartography/models/airbyte/workspace.py +46 -0
  59. cartography/models/aws/cloudtrail/management_events.py +64 -0
  60. cartography/models/aws/cloudwatch/log_metric_filter.py +79 -0
  61. cartography/models/aws/codebuild/__init__.py +0 -0
  62. cartography/models/aws/codebuild/project.py +49 -0
  63. cartography/models/aws/ec2/networkinterfaces.py +2 -0
  64. cartography/models/aws/ec2/subnet_instance.py +2 -0
  65. cartography/models/aws/ec2/subnet_networkinterface.py +2 -0
  66. cartography/models/aws/ecs/containers.py +19 -0
  67. cartography/models/aws/ecs/task_definitions.py +38 -0
  68. cartography/models/aws/ecs/tasks.py +24 -1
  69. cartography/models/aws/efs/access_point.py +77 -0
  70. cartography/models/aws/inspector/findings.py +37 -0
  71. cartography/models/aws/inspector/packages.py +1 -31
  72. cartography/models/aws/sns/topic_subscription.py +74 -0
  73. cartography/models/core/common.py +1 -0
  74. cartography/models/core/relationships.py +44 -0
  75. cartography/models/entra/user.py +17 -51
  76. cartography/models/scaleway/__init__.py +0 -0
  77. cartography/models/scaleway/iam/__init__.py +0 -0
  78. cartography/models/scaleway/iam/apikey.py +96 -0
  79. cartography/models/scaleway/iam/application.py +52 -0
  80. cartography/models/scaleway/iam/group.py +95 -0
  81. cartography/models/scaleway/iam/user.py +60 -0
  82. cartography/models/scaleway/instance/__init__.py +0 -0
  83. cartography/models/scaleway/instance/flexibleip.py +52 -0
  84. cartography/models/scaleway/instance/instance.py +118 -0
  85. cartography/models/scaleway/organization.py +19 -0
  86. cartography/models/scaleway/project.py +48 -0
  87. cartography/models/scaleway/storage/__init__.py +0 -0
  88. cartography/models/scaleway/storage/snapshot.py +78 -0
  89. cartography/models/scaleway/storage/volume.py +51 -0
  90. cartography/models/sentinelone/__init__.py +1 -0
  91. cartography/models/sentinelone/account.py +40 -0
  92. cartography/models/sentinelone/agent.py +50 -0
  93. cartography/models/sentinelone/application.py +44 -0
  94. cartography/models/sentinelone/application_version.py +96 -0
  95. cartography/sync.py +11 -4
  96. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/METADATA +20 -16
  97. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/RECORD +101 -36
  98. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/WHEEL +0 -0
  99. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/entry_points.txt +0 -0
  100. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/licenses/LICENSE +0 -0
  101. {cartography-0.106.0rc1.dist-info → cartography-0.107.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,92 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import scaleway
6
+ from scaleway.instance.v1 import InstanceV1API
7
+ from scaleway.instance.v1 import Server
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.scaleway.utils import DEFAULT_ZONE
12
+ from cartography.intel.scaleway.utils import scaleway_obj_to_dict
13
+ from cartography.models.scaleway.instance.instance import ScalewayInstanceSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ client: scaleway.Client,
23
+ common_job_parameters: dict[str, Any],
24
+ org_id: str,
25
+ projects_id: list[str],
26
+ update_tag: int,
27
+ ) -> None:
28
+ instances = get(client, org_id)
29
+ instances_by_project = transform_instances(instances)
30
+ load_instances(neo4j_session, instances_by_project, update_tag)
31
+ cleanup(neo4j_session, projects_id, common_job_parameters)
32
+
33
+
34
+ @timeit
35
+ def get(
36
+ client: scaleway.Client,
37
+ org_id: str,
38
+ ) -> list[Server]:
39
+ api = InstanceV1API(client)
40
+ return api.list_servers_all(organization=org_id, zone=DEFAULT_ZONE)
41
+
42
+
43
+ def transform_instances(
44
+ instances: list[Server],
45
+ ) -> dict[str, list[dict[str, Any]]]:
46
+ result: dict[str, list[dict[str, Any]]] = {}
47
+ for instance in instances:
48
+ project_id = instance.project
49
+ formatted_instance = scaleway_obj_to_dict(instance)
50
+ formatted_instance["public_ips"] = [
51
+ ip["id"] for ip in formatted_instance.get("public_ips", [])
52
+ ]
53
+ formatted_instance["volumes_id"] = [
54
+ volume["id"] for volume in formatted_instance.get("volumes", {}).values()
55
+ ]
56
+ result.setdefault(project_id, []).append(formatted_instance)
57
+ return result
58
+
59
+
60
+ @timeit
61
+ def load_instances(
62
+ neo4j_session: neo4j.Session,
63
+ data: dict[str, list[dict[str, Any]]],
64
+ update_tag: int,
65
+ ) -> None:
66
+ for project_id, instances in data.items():
67
+ logger.info(
68
+ "Loading %d Scaleway Instance in project '%s' into Neo4j.",
69
+ len(instances),
70
+ project_id,
71
+ )
72
+ load(
73
+ neo4j_session,
74
+ ScalewayInstanceSchema(),
75
+ instances,
76
+ lastupdated=update_tag,
77
+ PROJECT_ID=project_id,
78
+ )
79
+
80
+
81
+ @timeit
82
+ def cleanup(
83
+ neo4j_session: neo4j.Session,
84
+ projects_id: list[str],
85
+ common_job_parameters: dict[str, Any],
86
+ ) -> None:
87
+ for project_id in projects_id:
88
+ scopped_job_parameters = common_job_parameters.copy()
89
+ scopped_job_parameters["PROJECT_ID"] = project_id
90
+ GraphJob.from_node_schema(ScalewayInstanceSchema(), scopped_job_parameters).run(
91
+ neo4j_session
92
+ )
@@ -0,0 +1,79 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import scaleway
6
+ from scaleway.account.v3 import AccountV3ProjectAPI
7
+ from scaleway.account.v3 import Project
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.scaleway.utils import scaleway_obj_to_dict
12
+ from cartography.models.scaleway.organization import ScalewayOrganizationSchema
13
+ from cartography.models.scaleway.project import ScalewayProjectSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ client: scaleway.Client,
23
+ common_job_parameters: dict[str, Any],
24
+ org_id: str,
25
+ update_tag: int,
26
+ ) -> list[dict]:
27
+ projects = get(client, org_id)
28
+ formatted_projects = transform_projects(projects)
29
+ load_projects(neo4j_session, formatted_projects, org_id, update_tag)
30
+ cleanup(neo4j_session, common_job_parameters)
31
+ return formatted_projects
32
+
33
+
34
+ @timeit
35
+ def get(
36
+ client: scaleway.Client,
37
+ org_id: str,
38
+ ) -> list[Project]:
39
+ api = AccountV3ProjectAPI(client)
40
+ return api.list_projects_all(organization_id=org_id)
41
+
42
+
43
+ def transform_projects(projects: list[Project]) -> list[dict[str, Any]]:
44
+ formatted_projects = []
45
+ for project in projects:
46
+ formatted_projects.append(scaleway_obj_to_dict(project))
47
+ return formatted_projects
48
+
49
+
50
+ @timeit
51
+ def load_projects(
52
+ neo4j_session: neo4j.Session,
53
+ data: list[dict[str, Any]],
54
+ org_id: str,
55
+ update_tag: int,
56
+ ) -> None:
57
+ load(
58
+ neo4j_session,
59
+ ScalewayOrganizationSchema(),
60
+ [{"id": org_id}],
61
+ lastupdated=update_tag,
62
+ )
63
+ logger.info("Loading %d Scaleway Projects into Neo4j.", len(data))
64
+ load(
65
+ neo4j_session,
66
+ ScalewayProjectSchema(),
67
+ data,
68
+ lastupdated=update_tag,
69
+ ORG_ID=org_id,
70
+ )
71
+
72
+
73
+ @timeit
74
+ def cleanup(
75
+ neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
76
+ ) -> None:
77
+ GraphJob.from_node_schema(ScalewayProjectSchema(), common_job_parameters).run(
78
+ neo4j_session
79
+ )
File without changes
@@ -0,0 +1,86 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import scaleway
6
+ from scaleway.instance.v1 import InstanceV1API
7
+ from scaleway.instance.v1 import Snapshot
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.scaleway.utils import DEFAULT_ZONE
12
+ from cartography.intel.scaleway.utils import scaleway_obj_to_dict
13
+ from cartography.models.scaleway.storage.snapshot import ScalewayVolumeSnapshotSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ client: scaleway.Client,
23
+ common_job_parameters: dict[str, Any],
24
+ org_id: str,
25
+ projects_id: list[str],
26
+ update_tag: int,
27
+ ) -> None:
28
+ snapshots = get(client, org_id)
29
+ snapshots_by_project = transform_snapshots(snapshots)
30
+ load_snapshots(neo4j_session, snapshots_by_project, update_tag)
31
+ cleanup(neo4j_session, projects_id, common_job_parameters)
32
+
33
+
34
+ @timeit
35
+ def get(
36
+ client: scaleway.Client,
37
+ org_id: str,
38
+ ) -> list[Snapshot]:
39
+ api = InstanceV1API(client)
40
+ return api.list_snapshots_all(organization=org_id, zone=DEFAULT_ZONE)
41
+
42
+
43
+ def transform_snapshots(
44
+ snapshots: list[Snapshot],
45
+ ) -> dict[str, list[dict[str, Any]]]:
46
+ result: dict[str, list[dict[str, Any]]] = {}
47
+ for snapshot in snapshots:
48
+ project_id = snapshot.project
49
+ formatted_snapshot = scaleway_obj_to_dict(snapshot)
50
+ result.setdefault(project_id, []).append(formatted_snapshot)
51
+ return result
52
+
53
+
54
+ @timeit
55
+ def load_snapshots(
56
+ neo4j_session: neo4j.Session,
57
+ data: dict[str, list[dict[str, Any]]],
58
+ update_tag: int,
59
+ ) -> None:
60
+ for project_id, snapshots in data.items():
61
+ logger.info(
62
+ "Loading %d Scaleway InstanceSnapshots in project '%s' into Neo4j.",
63
+ len(snapshots),
64
+ project_id,
65
+ )
66
+ load(
67
+ neo4j_session,
68
+ ScalewayVolumeSnapshotSchema(),
69
+ snapshots,
70
+ lastupdated=update_tag,
71
+ PROJECT_ID=project_id,
72
+ )
73
+
74
+
75
+ @timeit
76
+ def cleanup(
77
+ neo4j_session: neo4j.Session,
78
+ projects_id: list[str],
79
+ common_job_parameters: dict[str, Any],
80
+ ) -> None:
81
+ for project_id in projects_id:
82
+ scoped_job_parameters = common_job_parameters.copy()
83
+ scoped_job_parameters["PROJECT_ID"] = project_id
84
+ GraphJob.from_node_schema(
85
+ ScalewayVolumeSnapshotSchema(), scoped_job_parameters
86
+ ).run(neo4j_session)
@@ -0,0 +1,84 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ import scaleway
6
+ from scaleway.instance.v1 import InstanceV1API
7
+ from scaleway.instance.v1 import Volume
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.scaleway.utils import DEFAULT_ZONE
12
+ from cartography.intel.scaleway.utils import scaleway_obj_to_dict
13
+ from cartography.models.scaleway.storage.volume import ScalewayVolumeSchema
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def sync(
21
+ neo4j_session: neo4j.Session,
22
+ client: scaleway.Client,
23
+ common_job_parameters: dict[str, Any],
24
+ org_id: str,
25
+ projects_id: list[str],
26
+ update_tag: int,
27
+ ) -> None:
28
+ volumes = get(client, org_id)
29
+ volumes_by_project = transform_volumes(volumes)
30
+ load_volumes(neo4j_session, volumes_by_project, update_tag)
31
+ cleanup(neo4j_session, projects_id, common_job_parameters)
32
+
33
+
34
+ @timeit
35
+ def get(
36
+ client: scaleway.Client,
37
+ org_id: str,
38
+ ) -> list[Volume]:
39
+ api = InstanceV1API(client)
40
+ return api.list_volumes_all(organization=org_id, zone=DEFAULT_ZONE)
41
+
42
+
43
+ def transform_volumes(volumes: list[Volume]) -> dict[str, list[dict[str, Any]]]:
44
+ result: dict[str, list[dict[str, Any]]] = {}
45
+ for volume in volumes:
46
+ project_id = volume.project
47
+ formatted_volume = scaleway_obj_to_dict(volume)
48
+ result.setdefault(project_id, []).append(formatted_volume)
49
+ return result
50
+
51
+
52
+ @timeit
53
+ def load_volumes(
54
+ neo4j_session: neo4j.Session,
55
+ data: dict[str, list[dict[str, Any]]],
56
+ update_tag: int,
57
+ ) -> None:
58
+ for project_id, volumes in data.items():
59
+ logger.info(
60
+ "Loading %d Scaleway InstanceVolumes in project '%s' into Neo4j.",
61
+ len(volumes),
62
+ project_id,
63
+ )
64
+ load(
65
+ neo4j_session,
66
+ ScalewayVolumeSchema(),
67
+ volumes,
68
+ lastupdated=update_tag,
69
+ PROJECT_ID=project_id,
70
+ )
71
+
72
+
73
+ @timeit
74
+ def cleanup(
75
+ neo4j_session: neo4j.Session,
76
+ projects_id: list[str],
77
+ common_job_parameters: dict[str, Any],
78
+ ) -> None:
79
+ for project_id in projects_id:
80
+ scoped_job_parameters = common_job_parameters.copy()
81
+ scoped_job_parameters["PROJECT_ID"] = project_id
82
+ GraphJob.from_node_schema(ScalewayVolumeSchema(), scoped_job_parameters).run(
83
+ neo4j_session
84
+ )
@@ -0,0 +1,37 @@
1
+ import dataclasses
2
+ from typing import Any
3
+
4
+ # Zone does not really matter for readonly access, but we need to set it
5
+ DEFAULT_ZONE = "fr-par-1"
6
+
7
+
8
+ def scaleway_obj_to_dict(obj: Any) -> dict[str, Any]:
9
+ """Transform a Scaleway object (dataclass, dict, or list) into a dictionary."""
10
+ if isinstance(obj, type) or not dataclasses.is_dataclass(obj):
11
+ raise TypeError(f"Expected a dataclass, got {type(obj).__name__} instead.")
12
+ result: dict[str, Any] = dataclasses.asdict(obj)
13
+
14
+ for k in list(result.keys()):
15
+ result[k] = _scaleway_element_sanitize(result[k])
16
+ return result
17
+
18
+
19
+ def _scaleway_element_sanitize(element: Any) -> Any:
20
+ """Sanitize a Scaleway element by removing empty strings and lists."""
21
+ if isinstance(element, str) and element == "":
22
+ return None
23
+ elif isinstance(element, list):
24
+ if len(element) == 0:
25
+ return None
26
+ return [
27
+ _scaleway_element_sanitize(item) for item in element if item is not None
28
+ ]
29
+ elif isinstance(element, dict):
30
+ return {
31
+ k: _scaleway_element_sanitize(v)
32
+ for k, v in element.items()
33
+ if v is not None
34
+ }
35
+ elif dataclasses.is_dataclass(element):
36
+ return scaleway_obj_to_dict(element)
37
+ return element
@@ -0,0 +1,69 @@
1
+ import logging
2
+
3
+ import neo4j
4
+
5
+ import cartography.intel.sentinelone.agent
6
+ import cartography.intel.sentinelone.application
7
+ from cartography.config import Config
8
+ from cartography.intel.sentinelone.account import sync_accounts
9
+ from cartography.stats import get_stats_client
10
+ from cartography.util import merge_module_sync_metadata
11
+ from cartography.util import timeit
12
+
13
+ logger = logging.getLogger(__name__)
14
+ stat_handler = get_stats_client(__name__)
15
+
16
+
17
+ @timeit
18
+ def start_sentinelone_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
19
+ """
20
+ Perform ingestion of SentinelOne data.
21
+ :param neo4j_session: Neo4j session for database interface
22
+ :param config: A cartography.config object
23
+ :return: None
24
+ """
25
+ if not config.sentinelone_api_token or not config.sentinelone_api_url:
26
+ logger.info("SentinelOne API configuration not found - skipping this module.")
27
+ return
28
+
29
+ # Set up common job parameters
30
+ common_job_parameters = {
31
+ "UPDATE_TAG": config.update_tag,
32
+ "API_URL": config.sentinelone_api_url,
33
+ "API_TOKEN": config.sentinelone_api_token,
34
+ }
35
+
36
+ # Sync SentinelOne account data (needs to be done first to establish the account nodes)
37
+ synced_account_ids = sync_accounts(
38
+ neo4j_session,
39
+ common_job_parameters,
40
+ config.sentinelone_account_ids,
41
+ )
42
+
43
+ # Sync agents and applications for each account
44
+ for account_id in synced_account_ids:
45
+ # Add account-specific parameter
46
+ common_job_parameters["S1_ACCOUNT_ID"] = account_id
47
+
48
+ cartography.intel.sentinelone.agent.sync(
49
+ neo4j_session,
50
+ common_job_parameters,
51
+ )
52
+
53
+ cartography.intel.sentinelone.application.sync(
54
+ neo4j_session,
55
+ common_job_parameters,
56
+ )
57
+
58
+ # Clean up account-specific parameters
59
+ del common_job_parameters["S1_ACCOUNT_ID"]
60
+
61
+ # Record that the sync is complete
62
+ merge_module_sync_metadata(
63
+ neo4j_session,
64
+ group_type="SentinelOne",
65
+ group_id="sentinelone",
66
+ synced_type="SentinelOneData",
67
+ update_tag=config.update_tag,
68
+ stat_handler=stat_handler,
69
+ )
@@ -0,0 +1,140 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+
6
+ from cartography.client.core.tx import load
7
+ from cartography.intel.sentinelone.api import call_sentinelone_api
8
+ from cartography.models.sentinelone.account import S1AccountSchema
9
+ from cartography.util import timeit
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @timeit
15
+ def get_accounts(
16
+ api_url: str, api_token: str, account_ids: list[str] | None = None
17
+ ) -> list[dict[str, Any]]:
18
+ """
19
+ Get account data from SentinelOne API
20
+ :param api_url: The SentinelOne API URL
21
+ :param api_token: The SentinelOne API token
22
+ :param account_ids: Optional list of account IDs to filter for
23
+ :return: Raw account data from API
24
+ """
25
+ logger.info("Retrieving SentinelOne account data")
26
+
27
+ # Get accounts info
28
+ response = call_sentinelone_api(
29
+ api_url=api_url,
30
+ endpoint="web/api/v2.1/accounts",
31
+ api_token=api_token,
32
+ )
33
+
34
+ accounts_data = response.get("data", [])
35
+
36
+ # Filter accounts by ID if specified
37
+ if account_ids:
38
+ accounts_data = [
39
+ account for account in accounts_data if account.get("id") in account_ids
40
+ ]
41
+ logger.info(f"Filtered accounts data to {len(accounts_data)} matching accounts")
42
+
43
+ if accounts_data:
44
+ logger.info(
45
+ f"Retrieved SentinelOne account data: {len(accounts_data)} accounts"
46
+ )
47
+ else:
48
+ logger.warning("No SentinelOne accounts retrieved")
49
+
50
+ return accounts_data
51
+
52
+
53
+ def transform_accounts(accounts_data: list[dict[str, Any]]) -> list[dict[str, Any]]:
54
+ """
55
+ Transform raw account data into standardized format for Neo4j ingestion
56
+ :param accounts_data: Raw account data from API
57
+ :return: List of transformed account data
58
+ """
59
+ result: list[dict[str, Any]] = []
60
+
61
+ for account in accounts_data:
62
+ transformed_account = {
63
+ # Required fields - use direct access (will raise KeyError if missing)
64
+ "id": account["id"],
65
+ # Optional fields - use .get() with None default
66
+ "name": account.get("name"),
67
+ "account_type": account.get("accountType"),
68
+ "active_agents": account.get("activeAgents"),
69
+ "created_at": account.get("createdAt"),
70
+ "expiration": account.get("expiration"),
71
+ "number_of_sites": account.get("numberOfSites"),
72
+ "state": account.get("state"),
73
+ }
74
+ result.append(transformed_account)
75
+
76
+ return result
77
+
78
+
79
+ def load_accounts(
80
+ neo4j_session: neo4j.Session,
81
+ accounts_data: list[dict[str, Any]],
82
+ update_tag: int,
83
+ ) -> None:
84
+ """
85
+ Load SentinelOne account data into Neo4j using the data model
86
+ :param neo4j_session: Neo4j session
87
+ :param accounts_data: List of account data to load
88
+ :param update_tag: Update tag for tracking data freshness
89
+ """
90
+ if not accounts_data:
91
+ logger.warning("No account data provided to load_accounts")
92
+ return
93
+
94
+ load(
95
+ neo4j_session,
96
+ S1AccountSchema(),
97
+ accounts_data,
98
+ lastupdated=update_tag,
99
+ firstseen=update_tag,
100
+ )
101
+
102
+ logger.info(f"Loaded {len(accounts_data)} SentinelOne account nodes")
103
+
104
+
105
+ @timeit
106
+ def sync_accounts(
107
+ neo4j_session: neo4j.Session,
108
+ common_job_parameters: dict[str, Any],
109
+ account_ids: list[str] | None = None,
110
+ ) -> list[str]:
111
+ """
112
+ Sync SentinelOne account data using the modern sync pattern
113
+ :param neo4j_session: Neo4j session
114
+ :param api_url: SentinelOne API URL
115
+ :param api_token: SentinelOne API token
116
+ :param update_tag: Update tag for tracking data freshness
117
+ :param common_job_parameters: Job parameters for cleanup
118
+ :param account_ids: Optional list of account IDs to filter for
119
+ :return: List of synced account IDs
120
+ """
121
+ # 1. GET - Fetch data from API
122
+ accounts_raw_data = get_accounts(
123
+ common_job_parameters["API_URL"],
124
+ common_job_parameters["API_TOKEN"],
125
+ account_ids,
126
+ )
127
+
128
+ # 2. TRANSFORM - Shape data for ingestion
129
+ transformed_accounts = transform_accounts(accounts_raw_data)
130
+
131
+ # 3. LOAD - Ingest to Neo4j using data model
132
+ load_accounts(
133
+ neo4j_session,
134
+ transformed_accounts,
135
+ common_job_parameters["UPDATE_TAG"],
136
+ )
137
+
138
+ synced_account_ids = [account["id"] for account in transformed_accounts]
139
+ logger.info(f"Synced {len(synced_account_ids)} SentinelOne accounts")
140
+ return synced_account_ids