cartography 0.106.0rc2__py3-none-any.whl → 0.107.0rc1__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.
- cartography/_version.py +2 -2
- cartography/cli.py +131 -2
- cartography/config.py +42 -0
- cartography/driftdetect/cli.py +3 -2
- cartography/intel/airbyte/__init__.py +105 -0
- cartography/intel/airbyte/connections.py +120 -0
- cartography/intel/airbyte/destinations.py +81 -0
- cartography/intel/airbyte/organizations.py +59 -0
- cartography/intel/airbyte/sources.py +78 -0
- cartography/intel/airbyte/tags.py +64 -0
- cartography/intel/airbyte/users.py +106 -0
- cartography/intel/airbyte/util.py +122 -0
- cartography/intel/airbyte/workspaces.py +63 -0
- cartography/intel/aws/__init__.py +1 -0
- cartography/intel/aws/cloudtrail_management_events.py +364 -0
- cartography/intel/aws/codebuild.py +132 -0
- cartography/intel/aws/resources.py +4 -0
- cartography/intel/aws/sns.py +62 -2
- cartography/intel/entra/users.py +84 -42
- cartography/intel/scaleway/__init__.py +127 -0
- cartography/intel/scaleway/iam/__init__.py +0 -0
- cartography/intel/scaleway/iam/apikeys.py +71 -0
- cartography/intel/scaleway/iam/applications.py +71 -0
- cartography/intel/scaleway/iam/groups.py +71 -0
- cartography/intel/scaleway/iam/users.py +71 -0
- cartography/intel/scaleway/instances/__init__.py +0 -0
- cartography/intel/scaleway/instances/flexibleips.py +86 -0
- cartography/intel/scaleway/instances/instances.py +92 -0
- cartography/intel/scaleway/projects.py +79 -0
- cartography/intel/scaleway/storage/__init__.py +0 -0
- cartography/intel/scaleway/storage/snapshots.py +86 -0
- cartography/intel/scaleway/storage/volumes.py +84 -0
- cartography/intel/scaleway/utils.py +37 -0
- cartography/intel/sentinelone/__init__.py +63 -0
- cartography/intel/sentinelone/account.py +140 -0
- cartography/intel/sentinelone/agent.py +139 -0
- cartography/intel/sentinelone/api.py +113 -0
- cartography/intel/sentinelone/utils.py +9 -0
- cartography/models/airbyte/__init__.py +0 -0
- cartography/models/airbyte/connection.py +138 -0
- cartography/models/airbyte/destination.py +75 -0
- cartography/models/airbyte/organization.py +19 -0
- cartography/models/airbyte/source.py +75 -0
- cartography/models/airbyte/stream.py +74 -0
- cartography/models/airbyte/tag.py +69 -0
- cartography/models/airbyte/user.py +111 -0
- cartography/models/airbyte/workspace.py +46 -0
- cartography/models/aws/cloudtrail/management_events.py +64 -0
- cartography/models/aws/codebuild/__init__.py +0 -0
- cartography/models/aws/codebuild/project.py +49 -0
- cartography/models/aws/ecs/containers.py +19 -0
- cartography/models/aws/ecs/task_definitions.py +38 -0
- cartography/models/aws/sns/topic_subscription.py +74 -0
- cartography/models/entra/user.py +17 -51
- cartography/models/scaleway/__init__.py +0 -0
- cartography/models/scaleway/iam/__init__.py +0 -0
- cartography/models/scaleway/iam/apikey.py +96 -0
- cartography/models/scaleway/iam/application.py +52 -0
- cartography/models/scaleway/iam/group.py +95 -0
- cartography/models/scaleway/iam/user.py +60 -0
- cartography/models/scaleway/instance/__init__.py +0 -0
- cartography/models/scaleway/instance/flexibleip.py +52 -0
- cartography/models/scaleway/instance/instance.py +118 -0
- cartography/models/scaleway/organization.py +19 -0
- cartography/models/scaleway/project.py +48 -0
- cartography/models/scaleway/storage/__init__.py +0 -0
- cartography/models/scaleway/storage/snapshot.py +78 -0
- cartography/models/scaleway/storage/volume.py +51 -0
- cartography/models/sentinelone/__init__.py +1 -0
- cartography/models/sentinelone/account.py +40 -0
- cartography/models/sentinelone/agent.py +50 -0
- cartography/sync.py +11 -4
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.dist-info}/METADATA +20 -16
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.dist-info}/RECORD +78 -18
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc1.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,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import neo4j
|
|
4
|
+
|
|
5
|
+
import cartography.intel.sentinelone.agent
|
|
6
|
+
from cartography.config import Config
|
|
7
|
+
from cartography.intel.sentinelone.account import sync_accounts
|
|
8
|
+
from cartography.stats import get_stats_client
|
|
9
|
+
from cartography.util import merge_module_sync_metadata
|
|
10
|
+
from cartography.util import timeit
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
stat_handler = get_stats_client(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@timeit
|
|
17
|
+
def start_sentinelone_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Perform ingestion of SentinelOne data.
|
|
20
|
+
:param neo4j_session: Neo4j session for database interface
|
|
21
|
+
:param config: A cartography.config object
|
|
22
|
+
:return: None
|
|
23
|
+
"""
|
|
24
|
+
if not config.sentinelone_api_token or not config.sentinelone_api_url:
|
|
25
|
+
logger.info("SentinelOne API configuration not found - skipping this module.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
# Set up common job parameters
|
|
29
|
+
common_job_parameters = {
|
|
30
|
+
"UPDATE_TAG": config.update_tag,
|
|
31
|
+
"API_URL": config.sentinelone_api_url,
|
|
32
|
+
"API_TOKEN": config.sentinelone_api_token,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Sync SentinelOne account data (needs to be done first to establish the account nodes)
|
|
36
|
+
synced_account_ids = sync_accounts(
|
|
37
|
+
neo4j_session,
|
|
38
|
+
common_job_parameters,
|
|
39
|
+
config.sentinelone_account_ids,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Sync agents for each account
|
|
43
|
+
for account_id in synced_account_ids:
|
|
44
|
+
# Add account-specific parameter
|
|
45
|
+
common_job_parameters["S1_ACCOUNT_ID"] = account_id
|
|
46
|
+
|
|
47
|
+
cartography.intel.sentinelone.agent.sync(
|
|
48
|
+
neo4j_session,
|
|
49
|
+
common_job_parameters,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Clean up account-specific parameter
|
|
53
|
+
del common_job_parameters["S1_ACCOUNT_ID"]
|
|
54
|
+
|
|
55
|
+
# Record that the sync is complete
|
|
56
|
+
merge_module_sync_metadata(
|
|
57
|
+
neo4j_session,
|
|
58
|
+
group_type="SentinelOne",
|
|
59
|
+
group_id="sentinelone",
|
|
60
|
+
synced_type="SentinelOneData",
|
|
61
|
+
update_tag=config.update_tag,
|
|
62
|
+
stat_handler=stat_handler,
|
|
63
|
+
)
|
|
@@ -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
|