cartography 0.106.0rc2__py3-none-any.whl → 0.107.0rc2__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/inspector.py +77 -48
- 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/inspector/findings.py +37 -0
- cartography/models/aws/inspector/packages.py +1 -31
- 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.0rc2.dist-info}/METADATA +20 -16
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc2.dist-info}/RECORD +81 -21
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc2.dist-info}/WHEEL +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc2.dist-info}/entry_points.txt +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc2.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.106.0rc2.dist-info → cartography-0.107.0rc2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import neo4j
|
|
5
|
+
|
|
6
|
+
from cartography.client.core.tx import load
|
|
7
|
+
from cartography.graph.job import GraphJob
|
|
8
|
+
from cartography.intel.sentinelone.api import get_paginated_results
|
|
9
|
+
from cartography.models.sentinelone.agent import S1AgentSchema
|
|
10
|
+
from cartography.util import timeit
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@timeit
|
|
16
|
+
def get_agents(api_url: str, api_token: str, account_id: str) -> list[dict[str, Any]]:
|
|
17
|
+
"""
|
|
18
|
+
Get agent data from SentinelOne API
|
|
19
|
+
:param api_url: The SentinelOne API URL
|
|
20
|
+
:param api_token: The SentinelOne API token
|
|
21
|
+
:param account_id: The SentinelOne account ID
|
|
22
|
+
:return: Raw agent data from API
|
|
23
|
+
"""
|
|
24
|
+
logger.info(f"Retrieving SentinelOne agent data for account {account_id}")
|
|
25
|
+
|
|
26
|
+
agents = get_paginated_results(
|
|
27
|
+
api_url=api_url,
|
|
28
|
+
endpoint="web/api/v2.1/agents",
|
|
29
|
+
api_token=api_token,
|
|
30
|
+
params={
|
|
31
|
+
"accountIds": account_id,
|
|
32
|
+
"limit": 1000,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger.info(f"Retrieved {len(agents)} agents from SentinelOne account {account_id}")
|
|
37
|
+
return agents
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@timeit
|
|
41
|
+
def transform_agents(agent_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
42
|
+
"""
|
|
43
|
+
Transform SentinelOne agent data for loading into Neo4j
|
|
44
|
+
:param agent_list: Raw agent data from the API
|
|
45
|
+
:return: Transformed agent data
|
|
46
|
+
"""
|
|
47
|
+
result: list[dict[str, Any]] = []
|
|
48
|
+
|
|
49
|
+
for agent in agent_list:
|
|
50
|
+
transformed_agent = {
|
|
51
|
+
# Required fields - use direct access (will raise KeyError if missing)
|
|
52
|
+
"id": agent["id"],
|
|
53
|
+
# Optional fields - use .get() with None default
|
|
54
|
+
"uuid": agent.get("uuid"),
|
|
55
|
+
"computer_name": agent.get("computerName"),
|
|
56
|
+
"firewall_enabled": agent.get("firewallEnabled"),
|
|
57
|
+
"os_name": agent.get("osName"),
|
|
58
|
+
"os_revision": agent.get("osRevision"),
|
|
59
|
+
"domain": agent.get("domain"),
|
|
60
|
+
"last_active": agent.get("lastActiveDate"),
|
|
61
|
+
"last_successful_scan": agent.get("lastSuccessfulScanDate"),
|
|
62
|
+
"scan_status": agent.get("scanStatus"),
|
|
63
|
+
"serial_number": agent.get("serialNumber"),
|
|
64
|
+
}
|
|
65
|
+
result.append(transformed_agent)
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@timeit
|
|
71
|
+
def load_agents(
|
|
72
|
+
neo4j_session: neo4j.Session,
|
|
73
|
+
data: list[dict[str, Any]],
|
|
74
|
+
account_id: str,
|
|
75
|
+
update_tag: int,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Load SentinelOne agent data into Neo4j
|
|
79
|
+
:param neo4j_session: Neo4j session
|
|
80
|
+
:param data: The transformed agent data
|
|
81
|
+
:param account_id: The SentinelOne account ID
|
|
82
|
+
:param update_tag: Update tag for tracking data freshness
|
|
83
|
+
:return: None
|
|
84
|
+
"""
|
|
85
|
+
logger.info(
|
|
86
|
+
f"Loading {len(data)} SentinelOne agents into Neo4j for account {account_id}"
|
|
87
|
+
)
|
|
88
|
+
load(
|
|
89
|
+
neo4j_session,
|
|
90
|
+
S1AgentSchema(),
|
|
91
|
+
data,
|
|
92
|
+
lastupdated=update_tag,
|
|
93
|
+
S1_ACCOUNT_ID=account_id,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@timeit
|
|
98
|
+
def cleanup(
|
|
99
|
+
neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Remove stale SentinelOne agent data from Neo4j
|
|
103
|
+
:param neo4j_session: Neo4j session
|
|
104
|
+
:param common_job_parameters: Common job parameters for cleanup
|
|
105
|
+
:return: None
|
|
106
|
+
"""
|
|
107
|
+
logger.debug("Running S1Agent cleanup job")
|
|
108
|
+
GraphJob.from_node_schema(S1AgentSchema(), common_job_parameters).run(neo4j_session)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@timeit
|
|
112
|
+
def sync(
|
|
113
|
+
neo4j_session: neo4j.Session,
|
|
114
|
+
common_job_parameters: dict[str, Any],
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Sync SentinelOne agents using the standard sync pattern
|
|
118
|
+
:param neo4j_session: Neo4j session
|
|
119
|
+
:param common_job_parameters: Common job parameters containing API_URL, API_TOKEN, S1_ACCOUNT_ID, UPDATE_TAG
|
|
120
|
+
:return: None
|
|
121
|
+
"""
|
|
122
|
+
api_url = common_job_parameters["API_URL"]
|
|
123
|
+
api_token = common_job_parameters["API_TOKEN"]
|
|
124
|
+
account_id = common_job_parameters["S1_ACCOUNT_ID"]
|
|
125
|
+
update_tag = common_job_parameters["UPDATE_TAG"]
|
|
126
|
+
|
|
127
|
+
logger.info(f"Syncing SentinelOne agent data for account {account_id}")
|
|
128
|
+
|
|
129
|
+
# 1. GET - Fetch data from API
|
|
130
|
+
agents_raw_data = get_agents(api_url, api_token, account_id)
|
|
131
|
+
|
|
132
|
+
# 2. TRANSFORM - Shape data for ingestion
|
|
133
|
+
transformed_data = transform_agents(agents_raw_data)
|
|
134
|
+
|
|
135
|
+
# 3. LOAD - Ingest to Neo4j using data model
|
|
136
|
+
load_agents(neo4j_session, transformed_data, account_id, update_tag)
|
|
137
|
+
|
|
138
|
+
# 4. CLEANUP - Remove stale data
|
|
139
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from cartography.util import timeit
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
# Connect and read timeouts of 60 seconds each
|
|
10
|
+
_TIMEOUT = (60, 60)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@timeit
|
|
14
|
+
def call_sentinelone_api(
|
|
15
|
+
api_url: str,
|
|
16
|
+
endpoint: str,
|
|
17
|
+
api_token: str,
|
|
18
|
+
method: str = "GET",
|
|
19
|
+
params: dict[str, Any] | None = None,
|
|
20
|
+
data: dict[str, Any] | None = None,
|
|
21
|
+
) -> dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Call the SentinelOne API
|
|
24
|
+
:param api_url: The base URL for the SentinelOne API
|
|
25
|
+
:param endpoint: The API endpoint to call
|
|
26
|
+
:param api_token: The API token for authentication
|
|
27
|
+
:param method: The HTTP method to use (default is GET)
|
|
28
|
+
:param params: Query parameters to include in the request
|
|
29
|
+
:param data: Data to include in the request body for POST/PUT methods
|
|
30
|
+
:return: The JSON response from the API
|
|
31
|
+
"""
|
|
32
|
+
full_url = f"{api_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
33
|
+
|
|
34
|
+
headers = {
|
|
35
|
+
"Accept": "application/json",
|
|
36
|
+
"Authorization": f"ApiToken {api_token}",
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
logger.debug(
|
|
42
|
+
"SentinelOne: %s %s",
|
|
43
|
+
method,
|
|
44
|
+
full_url,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
response = requests.request(
|
|
48
|
+
method=method,
|
|
49
|
+
url=full_url,
|
|
50
|
+
headers=headers,
|
|
51
|
+
params=params,
|
|
52
|
+
json=data,
|
|
53
|
+
timeout=_TIMEOUT,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Raise an exception for HTTP errors
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
|
|
59
|
+
except requests.exceptions.Timeout:
|
|
60
|
+
logger.warning(f"SentinelOne: Request to '{full_url}' timed out.")
|
|
61
|
+
raise
|
|
62
|
+
except requests.exceptions.RequestException as e:
|
|
63
|
+
logger.error(f"SentinelOne API request failed: {e}")
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
return response.json()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_paginated_results(
|
|
70
|
+
api_url: str,
|
|
71
|
+
endpoint: str,
|
|
72
|
+
api_token: str,
|
|
73
|
+
params: dict[str, Any] | None = None,
|
|
74
|
+
) -> list[dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
Handle cursor-based pagination for SentinelOne API requests
|
|
77
|
+
:param api_url: The base URL for the SentinelOne API
|
|
78
|
+
:param endpoint: The API endpoint to call
|
|
79
|
+
:param api_token: The API token for authentication
|
|
80
|
+
:param params: Query parameters to include in the request
|
|
81
|
+
:return: A list of all items from all pages
|
|
82
|
+
"""
|
|
83
|
+
query_params = params or {}
|
|
84
|
+
|
|
85
|
+
# Set default pagination parameters if not provided
|
|
86
|
+
if "limit" not in query_params:
|
|
87
|
+
query_params["limit"] = 100
|
|
88
|
+
|
|
89
|
+
next_cursor = None
|
|
90
|
+
total_items = []
|
|
91
|
+
|
|
92
|
+
while True:
|
|
93
|
+
if next_cursor:
|
|
94
|
+
query_params["cursor"] = next_cursor
|
|
95
|
+
|
|
96
|
+
response = call_sentinelone_api(
|
|
97
|
+
api_url=api_url,
|
|
98
|
+
endpoint=endpoint,
|
|
99
|
+
api_token=api_token,
|
|
100
|
+
params=query_params,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
items = response.get("data", [])
|
|
104
|
+
if not items:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
total_items.extend(items)
|
|
108
|
+
pagination = response.get("pagination", {})
|
|
109
|
+
next_cursor = pagination.get("nextCursor")
|
|
110
|
+
if not next_cursor:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
return total_items
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_application_id(name: str, vendor: str) -> str:
|
|
5
|
+
name_normalized = name.strip().lower().replace(" ", "_")
|
|
6
|
+
vendor_normalized = vendor.strip().lower().replace(" ", "_")
|
|
7
|
+
name_normalized = re.sub(r"[^\w]", "", name_normalized)
|
|
8
|
+
vendor_normalized = re.sub(r"[^\w\s]", "", vendor_normalized)
|
|
9
|
+
return f"{vendor_normalized}:{name_normalized}"
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
8
|
+
from cartography.models.core.relationships import LinkDirection
|
|
9
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
10
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
11
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class AirbyteConnectionNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef("connectionId")
|
|
17
|
+
name: PropertyRef = PropertyRef("name")
|
|
18
|
+
namespace_format: PropertyRef = PropertyRef("namespaceFormat")
|
|
19
|
+
prefix: PropertyRef = PropertyRef("prefix")
|
|
20
|
+
status: PropertyRef = PropertyRef("status")
|
|
21
|
+
data_residency: PropertyRef = PropertyRef("dataResidency")
|
|
22
|
+
non_breaking_schema_updates_behavior: PropertyRef = PropertyRef(
|
|
23
|
+
"nonBreakingSchemaUpdatesBehavior"
|
|
24
|
+
)
|
|
25
|
+
namespace_definition: PropertyRef = PropertyRef("namespaceDefinition")
|
|
26
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class AirbyteConnectionToOrganizationRelProperties(CartographyRelProperties):
|
|
31
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
# (:AirbyteOrganization)-[:RESOURCE]->(:AirbyteConnection)
|
|
36
|
+
class AirbyteConnectionToOrganizationRel(CartographyRelSchema):
|
|
37
|
+
target_node_label: str = "AirbyteOrganization"
|
|
38
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
39
|
+
{"id": PropertyRef("ORG_ID", set_in_kwargs=True)},
|
|
40
|
+
)
|
|
41
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
42
|
+
rel_label: str = "RESOURCE"
|
|
43
|
+
properties: AirbyteConnectionToOrganizationRelProperties = (
|
|
44
|
+
AirbyteConnectionToOrganizationRelProperties()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class AirbyteConnectionToWorkspaceRelProperties(CartographyRelProperties):
|
|
50
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
# (:AirbyteWorkspace)-[:CONTAINS]->(:AirbyteConnection)
|
|
55
|
+
class AirbyteConnectionToWorkspaceRel(CartographyRelSchema):
|
|
56
|
+
target_node_label: str = "AirbyteWorkspace"
|
|
57
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
58
|
+
{"id": PropertyRef("workspaceId")},
|
|
59
|
+
)
|
|
60
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
61
|
+
rel_label: str = "CONTAINS"
|
|
62
|
+
properties: AirbyteConnectionToWorkspaceRelProperties = (
|
|
63
|
+
AirbyteConnectionToWorkspaceRelProperties()
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class AirbyteConnectionToSourceRelProperties(CartographyRelProperties):
|
|
69
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
# (:AirbyteSource)<-[:SYNC_FROM]-(:AirbyteConnection)
|
|
74
|
+
class AirbyteConnectionToSourceRel(CartographyRelSchema):
|
|
75
|
+
target_node_label: str = "AirbyteSource"
|
|
76
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
77
|
+
{"id": PropertyRef("sourceId")},
|
|
78
|
+
)
|
|
79
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
80
|
+
rel_label: str = "SYNC_FROM"
|
|
81
|
+
properties: AirbyteConnectionToSourceRelProperties = (
|
|
82
|
+
AirbyteConnectionToSourceRelProperties()
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class AirbyteConnectionToDestinationRelProperties(CartographyRelProperties):
|
|
88
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
# (:AirbyteDestination)<-[:SYNC_TO]-(:AirbyteConnection)
|
|
93
|
+
class AirbyteConnectionToDestinationRel(CartographyRelSchema):
|
|
94
|
+
target_node_label: str = "AirbyteDestination"
|
|
95
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
96
|
+
{"id": PropertyRef("destinationId")},
|
|
97
|
+
)
|
|
98
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
99
|
+
rel_label: str = "SYNC_TO"
|
|
100
|
+
properties: AirbyteConnectionToDestinationRelProperties = (
|
|
101
|
+
AirbyteConnectionToDestinationRelProperties()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class AirbyteConnectionToTagRelProperties(CartographyRelProperties):
|
|
107
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
# (:AirbyteTag)<-[:TAGGED]-(:AirbyteConnection)
|
|
112
|
+
class AirbyteConnectionToTagRel(CartographyRelSchema):
|
|
113
|
+
target_node_label: str = "AirbyteTag"
|
|
114
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
115
|
+
{"id": PropertyRef("tags_ids", one_to_many=True)},
|
|
116
|
+
)
|
|
117
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
118
|
+
rel_label: str = "TAGGED"
|
|
119
|
+
properties: AirbyteConnectionToTagRelProperties = (
|
|
120
|
+
AirbyteConnectionToTagRelProperties()
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class AirbyteConnectionSchema(CartographyNodeSchema):
|
|
126
|
+
label: str = "AirbyteConnection"
|
|
127
|
+
properties: AirbyteConnectionNodeProperties = AirbyteConnectionNodeProperties()
|
|
128
|
+
sub_resource_relationship: AirbyteConnectionToOrganizationRel = (
|
|
129
|
+
AirbyteConnectionToOrganizationRel()
|
|
130
|
+
)
|
|
131
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
132
|
+
[
|
|
133
|
+
AirbyteConnectionToWorkspaceRel(),
|
|
134
|
+
AirbyteConnectionToSourceRel(),
|
|
135
|
+
AirbyteConnectionToDestinationRel(),
|
|
136
|
+
AirbyteConnectionToTagRel(),
|
|
137
|
+
]
|
|
138
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
8
|
+
from cartography.models.core.relationships import LinkDirection
|
|
9
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
10
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
11
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class AirbyteDestinationNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef("destinationId")
|
|
17
|
+
name: PropertyRef = PropertyRef("name")
|
|
18
|
+
type: PropertyRef = PropertyRef("destinationType")
|
|
19
|
+
config_host: PropertyRef = PropertyRef("configuration.host")
|
|
20
|
+
config_port: PropertyRef = PropertyRef("configuration.port")
|
|
21
|
+
config_name: PropertyRef = PropertyRef("configuration.name")
|
|
22
|
+
config_region: PropertyRef = PropertyRef("configuration.region")
|
|
23
|
+
config_endpoint: PropertyRef = PropertyRef("configuration.endpoint")
|
|
24
|
+
config_account: PropertyRef = PropertyRef("configuration.account")
|
|
25
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class AirbyteDestinationToOrganizationRelProperties(CartographyRelProperties):
|
|
30
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
# (:AirbyteOrganization)-[:RESOURCE]->(:AirbyteDestination)
|
|
35
|
+
class AirbyteDestinationToOrganizationRel(CartographyRelSchema):
|
|
36
|
+
target_node_label: str = "AirbyteOrganization"
|
|
37
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
38
|
+
{"id": PropertyRef("ORG_ID", set_in_kwargs=True)},
|
|
39
|
+
)
|
|
40
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
41
|
+
rel_label: str = "RESOURCE"
|
|
42
|
+
properties: AirbyteDestinationToOrganizationRelProperties = (
|
|
43
|
+
AirbyteDestinationToOrganizationRelProperties()
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class AirbyteDestinationToWorkspaceRelProperties(CartographyRelProperties):
|
|
49
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
# (:AirbyteWorkspace)-[:CONTAINS]->(:AirbyteDestination)
|
|
54
|
+
class AirbyteDestinationToWorkspaceRel(CartographyRelSchema):
|
|
55
|
+
target_node_label: str = "AirbyteWorkspace"
|
|
56
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
57
|
+
{"id": PropertyRef("workspaceId")},
|
|
58
|
+
)
|
|
59
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
60
|
+
rel_label: str = "CONTAINS"
|
|
61
|
+
properties: AirbyteDestinationToWorkspaceRelProperties = (
|
|
62
|
+
AirbyteDestinationToWorkspaceRelProperties()
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class AirbyteDestinationSchema(CartographyNodeSchema):
|
|
68
|
+
label: str = "AirbyteDestination"
|
|
69
|
+
properties: AirbyteDestinationNodeProperties = AirbyteDestinationNodeProperties()
|
|
70
|
+
sub_resource_relationship: AirbyteDestinationToOrganizationRel = (
|
|
71
|
+
AirbyteDestinationToOrganizationRel()
|
|
72
|
+
)
|
|
73
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
74
|
+
[AirbyteDestinationToWorkspaceRel()]
|
|
75
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from cartography.models.core.common import PropertyRef
|
|
4
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
5
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class AirbyteOrganizationNodeProperties(CartographyNodeProperties):
|
|
10
|
+
id: PropertyRef = PropertyRef("organizationId")
|
|
11
|
+
name: PropertyRef = PropertyRef("organizationName")
|
|
12
|
+
email: PropertyRef = PropertyRef("email")
|
|
13
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AirbyteOrganizationSchema(CartographyNodeSchema):
|
|
18
|
+
label: str = "AirbyteOrganization"
|
|
19
|
+
properties: AirbyteOrganizationNodeProperties = AirbyteOrganizationNodeProperties()
|