cartography 0.109.0rc2__py3-none-any.whl → 0.110.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 CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.109.0rc2'
21
- __version_tuple__ = version_tuple = (0, 109, 0, 'rc2')
20
+ __version__ = version = '0.110.0rc1'
21
+ __version_tuple__ = version_tuple = (0, 110, 0, 'rc1')
cartography/cli.py CHANGED
@@ -254,6 +254,14 @@ class CLI:
254
254
  "The name of environment variable containing Entra Client Secret for Service Principal Authentication."
255
255
  ),
256
256
  )
257
+ parser.add_argument(
258
+ "--entra-best-effort-mode",
259
+ action="store_true",
260
+ help=(
261
+ "Enable Entra ID sync best effort mode. This will allow cartography to continue "
262
+ "syncing other Entra ID entities and delay raising an exception until the end of the sync."
263
+ ),
264
+ )
257
265
  parser.add_argument(
258
266
  "--aws-requested-syncs",
259
267
  type=str,
@@ -700,6 +708,15 @@ class CLI:
700
708
  "Required if you are using the Trivy module. Ignored otherwise."
701
709
  ),
702
710
  )
711
+ parser.add_argument(
712
+ "--trivy-results-dir",
713
+ type=str,
714
+ default=None,
715
+ help=(
716
+ "Path to a directory containing Trivy JSON results on disk. "
717
+ "Required if you are using the Trivy module with local results."
718
+ ),
719
+ )
703
720
  parser.add_argument(
704
721
  "--scaleway-org",
705
722
  type=str,
@@ -1089,6 +1106,9 @@ class CLI:
1089
1106
  if config.trivy_s3_prefix:
1090
1107
  logger.debug(f"Trivy S3 prefix: {config.trivy_s3_prefix}")
1091
1108
 
1109
+ if config.trivy_results_dir:
1110
+ logger.debug(f"Trivy results dir: {config.trivy_results_dir}")
1111
+
1092
1112
  # Scaleway config
1093
1113
  if config.scaleway_secret_key_env_var:
1094
1114
  logger.debug(
@@ -1118,6 +1138,8 @@ class CLI:
1118
1138
  config.sentinelone_api_token = os.environ.get(
1119
1139
  config.sentinelone_api_token_env_var
1120
1140
  )
1141
+ else:
1142
+ config.sentinelone_api_token = None
1121
1143
 
1122
1144
  # Run cartography
1123
1145
  try:
cartography/config.py CHANGED
@@ -51,6 +51,9 @@ class Config:
51
51
  :param entra_client_id: Client Id for connecting in a Service Principal Authentication approach. Optional.
52
52
  :type entra_client_secret: str
53
53
  :param entra_client_secret: Client Secret for connecting in a Service Principal Authentication approach. Optional.
54
+ :type entra_best_effort_mode: bool
55
+ :param entra_best_effort_mode: If True, Entra ID sync will continue on errors and raise an aggregated
56
+ exception at the end of the sync. If False (default), exceptions will be raised immediately.
54
57
  :type aws_requested_syncs: str
55
58
  :param aws_requested_syncs: Comma-separated list of AWS resources to sync. Optional.
56
59
  :type aws_guardduty_severity_threshold: str
@@ -152,6 +155,8 @@ class Config:
152
155
  :param trivy_s3_bucket: The S3 bucket name containing Trivy scan results. Optional.
153
156
  :type trivy_s3_prefix: str
154
157
  :param trivy_s3_prefix: The S3 prefix path containing Trivy scan results. Optional.
158
+ :type trivy_results_dir: str
159
+ :param trivy_results_dir: Local directory containing Trivy scan results. Optional.
155
160
  :type scaleway_access_key: str
156
161
  :param scaleway_access_key: Scaleway access key. Optional.
157
162
  :type scaleway_secret_key: str
@@ -162,6 +167,8 @@ class Config:
162
167
  :param sentinelone_api_url: SentinelOne API URL. Optional.
163
168
  :type sentinelone_api_token: string
164
169
  :param sentinelone_api_token: SentinelOne API token for authentication. Optional.
170
+ :type sentinelone_api_token_env_var: string
171
+ :param sentinelone_api_token_env_var: The name of an environment variable containing the SentinelOne API token. Optional.
165
172
  :type sentinelone_account_ids: list[str]
166
173
  :param sentinelone_account_ids: List of SentinelOne account IDs to sync. Optional.
167
174
  """
@@ -187,6 +194,7 @@ class Config:
187
194
  entra_tenant_id=None,
188
195
  entra_client_id=None,
189
196
  entra_client_secret=None,
197
+ entra_best_effort_mode=False,
190
198
  aws_requested_syncs=None,
191
199
  aws_guardduty_severity_threshold=None,
192
200
  analysis_job_directory=None,
@@ -243,11 +251,13 @@ class Config:
243
251
  airbyte_api_url=None,
244
252
  trivy_s3_bucket=None,
245
253
  trivy_s3_prefix=None,
254
+ trivy_results_dir=None,
246
255
  scaleway_access_key=None,
247
256
  scaleway_secret_key=None,
248
257
  scaleway_org=None,
249
258
  sentinelone_api_url=None,
250
259
  sentinelone_api_token=None,
260
+ sentinelone_api_token_env_var=None,
251
261
  sentinelone_account_ids=None,
252
262
  ):
253
263
  self.neo4j_uri = neo4j_uri
@@ -271,6 +281,7 @@ class Config:
271
281
  self.entra_tenant_id = entra_tenant_id
272
282
  self.entra_client_id = entra_client_id
273
283
  self.entra_client_secret = entra_client_secret
284
+ self.entra_best_effort_mode = entra_best_effort_mode
274
285
  self.aws_requested_syncs = aws_requested_syncs
275
286
  self.aws_guardduty_severity_threshold = aws_guardduty_severity_threshold
276
287
  self.analysis_job_directory = analysis_job_directory
@@ -327,9 +338,11 @@ class Config:
327
338
  self.airbyte_api_url = airbyte_api_url
328
339
  self.trivy_s3_bucket = trivy_s3_bucket
329
340
  self.trivy_s3_prefix = trivy_s3_prefix
341
+ self.trivy_results_dir = trivy_results_dir
330
342
  self.scaleway_access_key = scaleway_access_key
331
343
  self.scaleway_secret_key = scaleway_secret_key
332
344
  self.scaleway_org = scaleway_org
333
345
  self.sentinelone_api_url = sentinelone_api_url
334
346
  self.sentinelone_api_token = sentinelone_api_token
347
+ self.sentinelone_api_token_env_var = sentinelone_api_token_env_var
335
348
  self.sentinelone_account_ids = sentinelone_account_ids
@@ -223,6 +223,13 @@ def transform_assume_role_events_to_role_assumptions(
223
223
 
224
224
  cloudtrail_event = json.loads(event["CloudTrailEvent"])
225
225
 
226
+ # Skip events with null requestParameters since we can't extract roleArn
227
+ if not cloudtrail_event.get("requestParameters"):
228
+ logger.debug(
229
+ f"Skipping CloudTrail AssumeRole event due to missing requestParameters. Event: {event.get('EventId', 'unknown')}"
230
+ )
231
+ continue
232
+
226
233
  if cloudtrail_event.get("userIdentity", {}).get("arn"):
227
234
  source_principal = cloudtrail_event["userIdentity"]["arn"]
228
235
  destination_principal = cloudtrail_event["requestParameters"]["roleArn"]
@@ -298,6 +305,13 @@ def transform_saml_role_events_to_role_assumptions(
298
305
 
299
306
  cloudtrail_event = json.loads(event["CloudTrailEvent"])
300
307
 
308
+ # Skip events with null requestParameters since we can't extract roleArn
309
+ if not cloudtrail_event.get("requestParameters"):
310
+ logger.debug(
311
+ f"Skipping CloudTrail AssumeRoleWithSAML event due to missing requestParameters. Event: {event.get('EventId', 'unknown')}"
312
+ )
313
+ continue
314
+
301
315
  response_elements = cloudtrail_event.get("responseElements", {})
302
316
  assumed_role_user = response_elements.get("assumedRoleUser", {})
303
317
 
@@ -370,6 +384,13 @@ def transform_web_identity_role_events_to_role_assumptions(
370
384
 
371
385
  cloudtrail_event = json.loads(event["CloudTrailEvent"])
372
386
 
387
+ # Skip events with null requestParameters since we can't extract roleArn
388
+ if not cloudtrail_event.get("requestParameters"):
389
+ logger.debug(
390
+ f"Skipping CloudTrail AssumeRoleWithWebIdentity event due to missing requestParameters. Event: {event.get('EventId', 'unknown')}"
391
+ )
392
+ continue
393
+
373
394
  user_identity = cloudtrail_event.get("userIdentity", {})
374
395
 
375
396
  if user_identity.get("type") == "WebIdentityUser" and user_identity.get(
@@ -0,0 +1,91 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+
6
+ import boto3
7
+ import neo4j
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.eventbridge.rule import EventBridgeRuleSchema
13
+ from cartography.util import aws_handle_regions
14
+ from cartography.util import timeit
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ @aws_handle_regions
21
+ def get_eventbridge_rules(
22
+ boto3_session: boto3.Session, region: str
23
+ ) -> List[Dict[str, Any]]:
24
+ client = boto3_session.client(
25
+ "events", region_name=region, config=get_botocore_config()
26
+ )
27
+ paginator = client.get_paginator("list_rules")
28
+ rules = []
29
+
30
+ for page in paginator.paginate():
31
+ rules.extend(page.get("Rules", []))
32
+
33
+ return rules
34
+
35
+
36
+ @timeit
37
+ def load_eventbridge_rules(
38
+ neo4j_session: neo4j.Session,
39
+ data: List[Dict[str, Any]],
40
+ region: str,
41
+ current_aws_account_id: str,
42
+ aws_update_tag: int,
43
+ ) -> None:
44
+ logger.info(
45
+ f"Loading EventBridge {len(data)} rules for region '{region}' into graph.",
46
+ )
47
+ load(
48
+ neo4j_session,
49
+ EventBridgeRuleSchema(),
50
+ data,
51
+ lastupdated=aws_update_tag,
52
+ Region=region,
53
+ AWS_ID=current_aws_account_id,
54
+ )
55
+
56
+
57
+ @timeit
58
+ def cleanup(
59
+ neo4j_session: neo4j.Session,
60
+ common_job_parameters: Dict[str, Any],
61
+ ) -> None:
62
+ logger.debug("Running EventBridge cleanup job.")
63
+ GraphJob.from_node_schema(EventBridgeRuleSchema(), common_job_parameters).run(
64
+ neo4j_session
65
+ )
66
+
67
+
68
+ @timeit
69
+ def sync(
70
+ neo4j_session: neo4j.Session,
71
+ boto3_session: boto3.session.Session,
72
+ regions: List[str],
73
+ current_aws_account_id: str,
74
+ update_tag: int,
75
+ common_job_parameters: Dict[str, Any],
76
+ ) -> None:
77
+ for region in regions:
78
+ logger.info(
79
+ f"Syncing EventBridge for region '{region}' in account '{current_aws_account_id}'.",
80
+ )
81
+
82
+ rules = get_eventbridge_rules(boto3_session, region)
83
+ load_eventbridge_rules(
84
+ neo4j_session,
85
+ rules,
86
+ region,
87
+ current_aws_account_id,
88
+ update_tag,
89
+ )
90
+
91
+ cleanup(neo4j_session, common_job_parameters)
@@ -18,6 +18,7 @@ from . import eks
18
18
  from . import elasticache
19
19
  from . import elasticsearch
20
20
  from . import emr
21
+ from . import eventbridge
21
22
  from . import glue
22
23
  from . import guardduty
23
24
  from . import iam
@@ -115,5 +116,6 @@ RESOURCE_FUNCTIONS: Dict[str, Callable[..., None]] = {
115
116
  "efs": efs.sync,
116
117
  "guardduty": guardduty.sync,
117
118
  "codebuild": codebuild.sync,
119
+ "eventbridge": eventbridge.sync,
118
120
  "glue": glue.sync,
119
121
  }
@@ -1,13 +1,14 @@
1
1
  import asyncio
2
+ import datetime
2
3
  import logging
4
+ from traceback import TracebackException
5
+ from typing import Awaitable
6
+ from typing import Callable
3
7
 
4
8
  import neo4j
5
9
 
6
10
  from cartography.config import Config
7
- from cartography.intel.entra.applications import sync_entra_applications
8
- from cartography.intel.entra.groups import sync_entra_groups
9
- from cartography.intel.entra.ou import sync_entra_ous
10
- from cartography.intel.entra.users import sync_entra_users
11
+ from cartography.intel.entra.resources import RESOURCE_FUNCTIONS
11
12
  from cartography.util import timeit
12
13
 
13
14
  logger = logging.getLogger(__name__)
@@ -39,45 +40,46 @@ def start_entra_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
39
40
  }
40
41
 
41
42
  async def main() -> None:
42
- # Run user sync
43
- await sync_entra_users(
44
- neo4j_session,
45
- config.entra_tenant_id,
46
- config.entra_client_id,
47
- config.entra_client_secret,
48
- config.update_tag,
49
- common_job_parameters,
50
- )
43
+ failed_stages = []
44
+ exception_tracebacks = []
51
45
 
52
- # Run group sync
53
- await sync_entra_groups(
54
- neo4j_session,
55
- config.entra_tenant_id,
56
- config.entra_client_id,
57
- config.entra_client_secret,
58
- config.update_tag,
59
- common_job_parameters,
60
- )
46
+ async def run_stage(name: str, func: Callable[..., Awaitable[None]]) -> None:
47
+ try:
48
+ await func(
49
+ neo4j_session,
50
+ config.entra_tenant_id,
51
+ config.entra_client_id,
52
+ config.entra_client_secret,
53
+ config.update_tag,
54
+ common_job_parameters,
55
+ )
56
+ except Exception as e:
57
+ if config.entra_best_effort_mode:
58
+ timestamp = datetime.datetime.now()
59
+ failed_stages.append(name)
60
+ exception_traceback = TracebackException.from_exception(e)
61
+ traceback_string = "".join(exception_traceback.format())
62
+ exception_tracebacks.append(
63
+ f"{timestamp} - Exception for stage {name}\n{traceback_string}"
64
+ )
65
+ logger.warning(
66
+ f"Caught exception syncing {name}. entra-best-effort-mode is on so we are continuing "
67
+ "on to the next Entra sync. All exceptions will be aggregated and re-logged at the end of the sync.",
68
+ exc_info=True,
69
+ )
70
+ else:
71
+ logger.error("Error during Entra sync", exc_info=True)
72
+ raise
61
73
 
62
- # Run OU sync
63
- await sync_entra_ous(
64
- neo4j_session,
65
- config.entra_tenant_id,
66
- config.entra_client_id,
67
- config.entra_client_secret,
68
- config.update_tag,
69
- common_job_parameters,
70
- )
74
+ for name, func in RESOURCE_FUNCTIONS:
75
+ await run_stage(name, func)
71
76
 
72
- # Run application sync
73
- await sync_entra_applications(
74
- neo4j_session,
75
- config.entra_tenant_id,
76
- config.entra_client_id,
77
- config.entra_client_secret,
78
- config.update_tag,
79
- common_job_parameters,
80
- )
77
+ if failed_stages:
78
+ logger.error(
79
+ f"Entra sync failed for the following stages: {', '.join(failed_stages)}. "
80
+ "See the logs for more details.",
81
+ )
82
+ raise Exception("\n".join(exception_tracebacks))
81
83
 
82
- # Execute both syncs in sequence
84
+ # Execute all syncs in sequence
83
85
  asyncio.run(main())
@@ -172,12 +172,11 @@ async def get_app_role_assignments(
172
172
  )
173
173
  continue
174
174
  except Exception as e:
175
- # Only catch truly unexpected errors - these should be rare
176
175
  logger.error(
177
176
  f"Unexpected error when fetching app role assignments for application {app.app_id} ({app.display_name}): {e}",
178
177
  exc_info=True,
179
178
  )
180
- continue
179
+ raise
181
180
 
182
181
  logger.info(f"Retrieved {len(assignments)} app role assignments total")
183
182
  return assignments
@@ -43,7 +43,7 @@ async def get_entra_ous(client: GraphServiceClient) -> list[AdministrativeUnit]:
43
43
  current_request = None
44
44
  except Exception as e:
45
45
  logger.error(f"Failed to retrieve administrative units: {str(e)}")
46
- current_request = None
46
+ raise
47
47
 
48
48
  return all_units
49
49
 
@@ -0,0 +1,20 @@
1
+ from cartography.intel.entra.applications import sync_entra_applications
2
+ from cartography.intel.entra.groups import sync_entra_groups
3
+ from cartography.intel.entra.ou import sync_entra_ous
4
+ from cartography.intel.entra.users import sync_entra_users
5
+
6
+ # This is a list so that we sync these resources in order.
7
+ # Data shape: [("resource_name", sync_function), ...]
8
+ # Each sync function will be called with the following arguments:
9
+ # - neo4j_session
10
+ # - config.entra_tenant_id
11
+ # - config.entra_client_id
12
+ # - config.entra_client_secret
13
+ # - config.update_tag
14
+ # - common_job_parameters
15
+ RESOURCE_FUNCTIONS = [
16
+ ("users", sync_entra_users),
17
+ ("groups", sync_entra_groups),
18
+ ("ous", sync_entra_ous),
19
+ ("applications", sync_entra_applications),
20
+ ]
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
  from typing import Any
3
4
 
@@ -8,7 +9,9 @@ from cartography.client.aws import list_accounts
8
9
  from cartography.client.aws.ecr import get_ecr_images
9
10
  from cartography.config import Config
10
11
  from cartography.intel.trivy.scanner import cleanup
12
+ from cartography.intel.trivy.scanner import get_json_files_in_dir
11
13
  from cartography.intel.trivy.scanner import get_json_files_in_s3
14
+ from cartography.intel.trivy.scanner import sync_single_image_from_file
12
15
  from cartography.intel.trivy.scanner import sync_single_image_from_s3
13
16
  from cartography.stats import get_stats_client
14
17
  from cartography.util import timeit
@@ -39,13 +42,13 @@ def get_scan_targets(
39
42
 
40
43
 
41
44
  def _get_intersection(
42
- images_in_graph: set[str], json_files: set[str], trivy_s3_prefix: str
45
+ image_uris: set[str], json_files: set[str], trivy_s3_prefix: str
43
46
  ) -> list[tuple[str, str]]:
44
47
  """
45
48
  Get the intersection of ECR images in the graph and S3 scan results.
46
49
 
47
50
  Args:
48
- images_in_graph: Set of ECR images in the graph
51
+ image_uris: Set of ECR images in the graph
49
52
  json_files: Set of S3 object keys for JSON files
50
53
  trivy_s3_prefix: S3 prefix path containing scan results
51
54
 
@@ -60,7 +63,7 @@ def _get_intersection(
60
63
  # Remove the prefix and the .json suffix
61
64
  image_uri = s3_object_key[prefix_len:-5]
62
65
 
63
- if image_uri in images_in_graph:
66
+ if image_uri in image_uris:
64
67
  intersection.append((image_uri, s3_object_key))
65
68
 
66
69
  return intersection
@@ -90,12 +93,12 @@ def sync_trivy_aws_ecr_from_s3(
90
93
  f"Using Trivy scan results from s3://{trivy_s3_bucket}/{trivy_s3_prefix}"
91
94
  )
92
95
 
93
- images_in_graph: set[str] = get_scan_targets(neo4j_session)
96
+ image_uris: set[str] = get_scan_targets(neo4j_session)
94
97
  json_files: set[str] = get_json_files_in_s3(
95
98
  trivy_s3_bucket, trivy_s3_prefix, boto3_session
96
99
  )
97
100
  intersection: list[tuple[str, str]] = _get_intersection(
98
- images_in_graph, json_files, trivy_s3_prefix
101
+ image_uris, json_files, trivy_s3_prefix
99
102
  )
100
103
 
101
104
  if len(intersection) == 0:
@@ -124,21 +127,79 @@ def sync_trivy_aws_ecr_from_s3(
124
127
  cleanup(neo4j_session, common_job_parameters)
125
128
 
126
129
 
130
+ @timeit
131
+ def sync_trivy_aws_ecr_from_dir(
132
+ neo4j_session: Session,
133
+ results_dir: str,
134
+ update_tag: int,
135
+ common_job_parameters: dict[str, Any],
136
+ ) -> None:
137
+ """Sync Trivy scan results from local files for AWS ECR images."""
138
+ logger.info(f"Using Trivy scan results from {results_dir}")
139
+
140
+ image_uris: set[str] = get_scan_targets(neo4j_session)
141
+ json_files: set[str] = get_json_files_in_dir(results_dir)
142
+
143
+ if not json_files:
144
+ logger.error(
145
+ f"Trivy sync was configured, but no json files were found in {results_dir}."
146
+ )
147
+ raise ValueError("No Trivy json results found on disk")
148
+
149
+ logger.info(f"Processing {len(json_files)} local Trivy result files")
150
+
151
+ for file_path in json_files:
152
+ # First, check if the image exists in the graph before syncing
153
+ try:
154
+ # Peek at the artifact name without processing the file
155
+ with open(file_path, encoding="utf-8") as f:
156
+ trivy_data = json.load(f)
157
+ artifact_name = trivy_data.get("ArtifactName")
158
+
159
+ if artifact_name and artifact_name not in image_uris:
160
+ logger.debug(
161
+ f"Skipping results for {artifact_name} since the image is not present in the graph"
162
+ )
163
+ continue
164
+
165
+ except (json.JSONDecodeError, KeyError) as e:
166
+ logger.error(f"Failed to read artifact name from {file_path}: {e}")
167
+ continue
168
+
169
+ # Now sync the file since we know the image exists in the graph
170
+ sync_single_image_from_file(
171
+ neo4j_session,
172
+ file_path,
173
+ update_tag,
174
+ )
175
+
176
+ cleanup(neo4j_session, common_job_parameters)
177
+
178
+
127
179
  @timeit
128
180
  def start_trivy_ingestion(neo4j_session: Session, config: Config) -> None:
129
- """
130
- Start Trivy scan ingestion from S3.
181
+ """Start Trivy scan ingestion from S3 or local files.
131
182
 
132
183
  Args:
133
184
  neo4j_session: Neo4j session for database operations
134
- config: Configuration object containing S3 settings
185
+ config: Configuration object containing S3 or directory paths
135
186
  """
136
- # Check if S3 configuration is provided
137
- if not config.trivy_s3_bucket:
138
- logger.info("Trivy S3 configuration not provided. Skipping Trivy ingestion.")
187
+ if not config.trivy_s3_bucket and not config.trivy_results_dir:
188
+ logger.info("Trivy configuration not provided. Skipping Trivy ingestion.")
189
+ return
190
+
191
+ if config.trivy_results_dir:
192
+ common_job_parameters = {
193
+ "UPDATE_TAG": config.update_tag,
194
+ }
195
+ sync_trivy_aws_ecr_from_dir(
196
+ neo4j_session,
197
+ config.trivy_results_dir,
198
+ config.update_tag,
199
+ common_job_parameters,
200
+ )
139
201
  return
140
202
 
141
- # Default to empty string if s3 prefix is not provided
142
203
  if config.trivy_s3_prefix is None:
143
204
  config.trivy_s3_prefix = ""
144
205
 
@@ -146,7 +207,6 @@ def start_trivy_ingestion(neo4j_session: Session, config: Config) -> None:
146
207
  "UPDATE_TAG": config.update_tag,
147
208
  }
148
209
 
149
- # Get ECR images to scan
150
210
  boto3_session = boto3.Session()
151
211
 
152
212
  sync_trivy_aws_ecr_from_s3(
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import os
3
4
  from typing import Any
4
5
 
5
6
  import boto3
@@ -127,6 +128,90 @@ def transform_scan_results(
127
128
  return findings_list, packages_list, fixes_list
128
129
 
129
130
 
131
+ def _parse_trivy_data(
132
+ trivy_data: dict, source: str
133
+ ) -> tuple[str | None, list[dict], str]:
134
+ """
135
+ Parse Trivy scan data and extract common fields.
136
+
137
+ Args:
138
+ trivy_data: Raw JSON Trivy data
139
+ source: Source identifier for error messages (file path or S3 URI)
140
+
141
+ Returns:
142
+ Tuple of (artifact_name, results, image_digest)
143
+ """
144
+ # Extract artifact name if present (only for file-based)
145
+ artifact_name = trivy_data.get("ArtifactName")
146
+
147
+ if "Results" not in trivy_data:
148
+ logger.error(
149
+ f"Scan data did not contain a `Results` key for {source}. This indicates a malformed scan result."
150
+ )
151
+ raise ValueError(f"Missing 'Results' key in scan data for {source}")
152
+
153
+ results = trivy_data["Results"]
154
+ if not results:
155
+ stat_handler.incr("image_scan_no_results_count")
156
+ logger.info(f"No vulnerabilities found for {source}")
157
+
158
+ if "Metadata" not in trivy_data or not trivy_data["Metadata"]:
159
+ raise ValueError(f"Missing 'Metadata' in scan data for {source}")
160
+
161
+ repo_digests = trivy_data["Metadata"].get("RepoDigests", [])
162
+ if not repo_digests:
163
+ raise ValueError(f"Missing 'RepoDigests' in scan metadata for {source}")
164
+
165
+ repo_digest = repo_digests[0]
166
+ if "@" not in repo_digest:
167
+ raise ValueError(f"Invalid repo digest format in {source}: {repo_digest}")
168
+
169
+ image_digest = repo_digest.split("@")[1]
170
+ if not image_digest:
171
+ raise ValueError(f"Empty image digest for {source}")
172
+
173
+ return artifact_name, results, image_digest
174
+
175
+
176
+ @timeit
177
+ def sync_single_image(
178
+ neo4j_session: Session,
179
+ trivy_data: dict,
180
+ source: str,
181
+ update_tag: int,
182
+ ) -> None:
183
+ """
184
+ Sync a single image's Trivy scan results to Neo4j.
185
+
186
+ Args:
187
+ neo4j_session: Neo4j session for database operations
188
+ trivy_data: Raw Trivy JSON data
189
+ source: Source identifier for logging (file path or image URI)
190
+ update_tag: Update tag for tracking
191
+ """
192
+ try:
193
+ _, results, image_digest = _parse_trivy_data(trivy_data, source)
194
+
195
+ # Transform all data in one pass
196
+ findings_list, packages_list, fixes_list = transform_scan_results(
197
+ results,
198
+ image_digest,
199
+ )
200
+
201
+ num_findings = len(findings_list)
202
+ stat_handler.incr("image_scan_cve_count", num_findings)
203
+
204
+ # Load the transformed data
205
+ load_scan_vulns(neo4j_session, findings_list, update_tag=update_tag)
206
+ load_scan_packages(neo4j_session, packages_list, update_tag=update_tag)
207
+ load_scan_fixes(neo4j_session, fixes_list, update_tag=update_tag)
208
+ stat_handler.incr("images_processed_count")
209
+
210
+ except Exception as e:
211
+ logger.error(f"Failed to process scan results for {source}: {e}")
212
+ raise
213
+
214
+
130
215
  @timeit
131
216
  def get_json_files_in_s3(
132
217
  s3_bucket: str, s3_prefix: str, boto3_session: boto3.Session
@@ -177,6 +262,18 @@ def get_json_files_in_s3(
177
262
  return results
178
263
 
179
264
 
265
+ @timeit
266
+ def get_json_files_in_dir(results_dir: str) -> set[str]:
267
+ """Return set of JSON file paths under a directory."""
268
+ results = set()
269
+ for root, _dirs, files in os.walk(results_dir):
270
+ for filename in files:
271
+ if filename.endswith(".json"):
272
+ results.add(os.path.join(root, filename))
273
+ logger.info(f"Found {len(results)} json files in {results_dir}")
274
+ return results
275
+
276
+
180
277
  @timeit
181
278
  def cleanup(neo4j_session: Session, common_job_parameters: dict[str, Any]) -> None:
182
279
  """
@@ -245,58 +342,6 @@ def load_scan_fixes(
245
342
  )
246
343
 
247
344
 
248
- @timeit
249
- def read_scan_results_from_s3(
250
- boto3_session: boto3.Session,
251
- s3_bucket: str,
252
- s3_object_key: str,
253
- image_uri: str,
254
- ) -> tuple[list[dict], str | None]:
255
- """
256
- Read and parse Trivy scan results from S3.
257
-
258
- Args:
259
- boto3_session: boto3 session for S3 operations
260
- s3_bucket: S3 bucket containing scan results
261
- s3_object_key: S3 object key for the scan results
262
- image_uri: ECR image URI (for logging purposes)
263
-
264
- Returns:
265
- Tuple of (list of scan result dictionaries from the "Results" key, image digest)
266
- """
267
- s3_client = boto3_session.client("s3")
268
-
269
- # Read JSON scan results from S3
270
- logger.debug(f"Reading scan results from S3: s3://{s3_bucket}/{s3_object_key}")
271
- response = s3_client.get_object(Bucket=s3_bucket, Key=s3_object_key)
272
- scan_data_json = response["Body"].read().decode("utf-8")
273
-
274
- # Parse JSON data
275
- trivy_data = json.loads(scan_data_json)
276
-
277
- # Extract results using the same logic as binary scanning
278
- if "Results" in trivy_data and trivy_data["Results"]:
279
- results = trivy_data["Results"]
280
- else:
281
- stat_handler.incr("image_scan_no_results_count")
282
- logger.warning(
283
- f"S3 scan data did not contain a `Results` key for URI = {image_uri}; continuing."
284
- )
285
- results = []
286
-
287
- image_digest = None
288
- if "Metadata" in trivy_data and trivy_data["Metadata"]:
289
- repo_digests = trivy_data["Metadata"].get("RepoDigests", [])
290
- if repo_digests:
291
- # Sample input: 000000000000.dkr.ecr.us-east-1.amazonaws.com/test-repository@sha256:88016
292
- # Sample output: sha256:88016
293
- repo_digest = repo_digests[0]
294
- if "@" in repo_digest:
295
- image_digest = repo_digest.split("@")[1]
296
-
297
- return results, image_digest
298
-
299
-
300
345
  @timeit
301
346
  def sync_single_image_from_s3(
302
347
  neo4j_session: Session,
@@ -317,47 +362,25 @@ def sync_single_image_from_s3(
317
362
  s3_object_key: S3 object key for this image's scan results
318
363
  boto3_session: boto3 session for S3 operations
319
364
  """
320
- try:
321
- # Read and parse scan results from S3
322
- results, image_digest = read_scan_results_from_s3(
323
- boto3_session,
324
- s3_bucket,
325
- s3_object_key,
326
- image_uri,
327
- )
328
- if not image_digest:
329
- logger.warning(f"No image digest found for {image_uri}; skipping over.")
330
- return
365
+ s3_client = boto3_session.client("s3")
331
366
 
332
- # Transform all data in one pass using existing function
333
- findings_list, packages_list, fixes_list = transform_scan_results(
334
- results,
335
- image_digest,
336
- )
367
+ logger.debug(f"Reading scan results from S3: s3://{s3_bucket}/{s3_object_key}")
368
+ response = s3_client.get_object(Bucket=s3_bucket, Key=s3_object_key)
369
+ scan_data_json = response["Body"].read().decode("utf-8")
337
370
 
338
- num_findings = len(findings_list)
339
- stat_handler.incr("image_scan_cve_count", num_findings)
371
+ trivy_data = json.loads(scan_data_json)
372
+ sync_single_image(neo4j_session, trivy_data, image_uri, update_tag)
340
373
 
341
- # Load the transformed data using existing functions
342
- load_scan_vulns(
343
- neo4j_session,
344
- findings_list,
345
- update_tag=update_tag,
346
- )
347
- load_scan_packages(
348
- neo4j_session,
349
- packages_list,
350
- update_tag=update_tag,
351
- )
352
- load_scan_fixes(
353
- neo4j_session,
354
- fixes_list,
355
- update_tag=update_tag,
356
- )
357
- stat_handler.incr("images_processed_count")
358
374
 
359
- except Exception as e:
360
- logger.error(
361
- f"Failed to process S3 scan results for {image_uri} from {s3_object_key}: {e}"
362
- )
363
- raise
375
+ @timeit
376
+ def sync_single_image_from_file(
377
+ neo4j_session: Session,
378
+ file_path: str,
379
+ update_tag: int,
380
+ ) -> None:
381
+ """Read a Trivy JSON file from disk and sync to Neo4j."""
382
+ logger.debug(f"Reading scan results from file: {file_path}")
383
+ with open(file_path, encoding="utf-8") as f:
384
+ trivy_data = json.load(f)
385
+
386
+ sync_single_image(neo4j_session, trivy_data, file_path, update_tag)
File without changes
@@ -0,0 +1,77 @@
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 EventBridgeRuleNodeProperties(CartographyNodeProperties):
16
+ id: PropertyRef = PropertyRef("Arn")
17
+ arn: PropertyRef = PropertyRef("Arn", extra_index=True)
18
+ name: PropertyRef = PropertyRef("Name")
19
+ region: PropertyRef = PropertyRef("Region", set_in_kwargs=True)
20
+ event_pattern: PropertyRef = PropertyRef("EventPattern")
21
+ state: PropertyRef = PropertyRef("State")
22
+ description: PropertyRef = PropertyRef("Description")
23
+ schedule_expression: PropertyRef = PropertyRef("ScheduleExpression")
24
+ role_arn: PropertyRef = PropertyRef("RoleArn")
25
+ managed_by: PropertyRef = PropertyRef("ManagedBy")
26
+ event_bus_name: PropertyRef = PropertyRef("EventBusName")
27
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class EventBridgeRuleToAwsAccountRelProperties(CartographyRelProperties):
32
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class EventBridgeRuleToAWSAccountRel(CartographyRelSchema):
37
+ target_node_label: str = "AWSAccount"
38
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
39
+ {"id": PropertyRef("AWS_ID", set_in_kwargs=True)},
40
+ )
41
+ direction: LinkDirection = LinkDirection.INWARD
42
+ rel_label: str = "RESOURCE"
43
+ properties: EventBridgeRuleToAwsAccountRelProperties = (
44
+ EventBridgeRuleToAwsAccountRelProperties()
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class EventBridgeRuleToAWSRoleRelProperties(CartographyRelProperties):
50
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class EventBridgeRuleToAWSRoleRel(CartographyRelSchema):
55
+ target_node_label: str = "AWSRole"
56
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
57
+ {"arn": PropertyRef("RoleArn")},
58
+ )
59
+ direction: LinkDirection = LinkDirection.OUTWARD
60
+ rel_label: str = "ASSOCIATED_WITH"
61
+ properties: EventBridgeRuleToAWSRoleRelProperties = (
62
+ EventBridgeRuleToAWSRoleRelProperties()
63
+ )
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class EventBridgeRuleSchema(CartographyNodeSchema):
68
+ label: str = "EventBridgeRule"
69
+ properties: EventBridgeRuleNodeProperties = EventBridgeRuleNodeProperties()
70
+ sub_resource_relationship: EventBridgeRuleToAWSAccountRel = (
71
+ EventBridgeRuleToAWSAccountRel()
72
+ )
73
+ other_relationships: OtherRelationships = OtherRelationships(
74
+ [
75
+ EventBridgeRuleToAWSRoleRel(),
76
+ ]
77
+ )
@@ -29,6 +29,7 @@ class SnipeitAssetNodeProperties(CartographyNodeProperties):
29
29
  manufacturer: PropertyRef = PropertyRef("manufacturer.name")
30
30
  model: PropertyRef = PropertyRef("model.name")
31
31
  serial: PropertyRef = PropertyRef("serial", extra_index=True)
32
+ status: PropertyRef = PropertyRef("status_label.name")
32
33
 
33
34
 
34
35
  ###
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cartography
3
- Version: 0.109.0rc2
3
+ Version: 0.110.0rc1
4
4
  Summary: Explore assets and their relationships across your technical infrastructure.
5
5
  Maintainer: Cartography Contributors
6
6
  License: apache2
@@ -1,8 +1,8 @@
1
1
  cartography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  cartography/__main__.py,sha256=y5iqUrj4BmqZfjeDT-9balzpXeMARgHeIedRMRI1gr8,100
3
- cartography/_version.py,sha256=nimLevl7Lt41ABvOl3VLASk_dXzF0KzyiM-BgKu2SmU,525
4
- cartography/cli.py,sha256=qOu3nSIKWsuDbuPe5XrKBoAArFQKZP8jSjK8OtI69_E,46595
5
- cartography/config.py,sha256=q-fVcWJH9ida_cAII9RcOT0FUpFFify7W6NMvmC3kGk,16873
3
+ cartography/_version.py,sha256=K77bmqqNrEr6adzs0e_JUxOx7UAa1mzTnIfy4pjE6lc,525
4
+ cartography/cli.py,sha256=S6O4FRxxBba60JAm6aLvQYiPS9DFxj0KLonWs4IU0Gw,47437
5
+ cartography/config.py,sha256=mRwjhJMSLp7BLxKmAAf96QZanpIaMK6GDImHPYlYFnM,17714
6
6
  cartography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  cartography/stats.py,sha256=N95prYlSzY8U52VFgKIDgOWOawu5Mig4N7GcVp3binQ,4420
8
8
  cartography/sync.py,sha256=-WsU6tGUKpfxPRndm1QU_0HQyE20Q7fexQWRHWFrnQI,13867
@@ -147,7 +147,7 @@ cartography/intel/aws/__init__.py,sha256=lgu0kxERg_dJGuktEDU0ImpHuNGHJharoLEpirK
147
147
  cartography/intel/aws/acm.py,sha256=a_i2pYPpocjlL8itwlcrmg8KrSLfjcmrkhcrPP-Omj8,3827
148
148
  cartography/intel/aws/apigateway.py,sha256=hOYVSY70DbrEfhi9gkX7PAMtg9WiPE0Pbp2wqGes7Vs,12198
149
149
  cartography/intel/aws/cloudtrail.py,sha256=LCqf3pQMiMY4h1Wehb_OjwXst2fMIEnNOD8HbXsK4X4,2753
150
- cartography/intel/aws/cloudtrail_management_events.py,sha256=e4P9HTqv7JKqJk-QtjWuFFt4X753PPw5JgtT5Y6GW2U,34732
150
+ cartography/intel/aws/cloudtrail_management_events.py,sha256=z5VZH1wQvl_ROG1sB9ZPdH4uexp-BKrI-WdEkhQKbkQ,35751
151
151
  cartography/intel/aws/cloudwatch.py,sha256=bBPlx5W15nun-jhYs-UAZQpo9Qm7FcBs4gBQevTqmi4,7246
152
152
  cartography/intel/aws/codebuild.py,sha256=8I-Xzm_c5-ixnGOOHIQLYYpClnaGjjHrEMjQ0-DGsgM,3958
153
153
  cartography/intel/aws/config.py,sha256=IIICicLQ0OTT3H3o8LDakIkA1BPUMwSyzpKonet-PaY,7658
@@ -159,6 +159,7 @@ cartography/intel/aws/eks.py,sha256=bPItyEj5q0nSDltJrr0S5MIrTPV0fK3xkqF6EV8fcqA,
159
159
  cartography/intel/aws/elasticache.py,sha256=ePTi-49Lbw94L6m7id5dUk1cG6FZRizFxojxKZBk8XY,5552
160
160
  cartography/intel/aws/elasticsearch.py,sha256=8X_Rp1zejkvXho0Zz_Cr4g-w9IpompdYRc-YS595Aso,8645
161
161
  cartography/intel/aws/emr.py,sha256=EJoKjHQP7-F_A1trUNU05Sb42yNR1i0C9VIpGcCfAXw,3662
162
+ cartography/intel/aws/eventbridge.py,sha256=XmF8Wfigbu94HOmpVR2w-fujpHjJi8HqJoBJ8wOZI0o,2285
162
163
  cartography/intel/aws/glue.py,sha256=-AQh1PIbWzi7y-uZbx2yaHnrEpMsENSBX-pGUFqc6xQ,3349
163
164
  cartography/intel/aws/guardduty.py,sha256=QpwWisz3TzbOxkRKNjByk4WWDCCMXr8jgNOgOdjQM5g,8532
164
165
  cartography/intel/aws/iam.py,sha256=bkyIzWw2OC4MHxuoCvTiZ5eEGEQhz2asiUgY_tkX2GY,34322
@@ -172,7 +173,7 @@ cartography/intel/aws/permission_relationships.py,sha256=LTmnHS6zk9hcdL548V5ka3E
172
173
  cartography/intel/aws/rds.py,sha256=are2LsWe8tM4UkCaGVnXS7F-ZVawklpc9h4lsM-fpGY,16173
173
174
  cartography/intel/aws/redshift.py,sha256=FGcCzcnm1OOrbJvLqtR1DwWVn1pt4Y6_eKkTUERT7Ws,7108
174
175
  cartography/intel/aws/resourcegroupstaggingapi.py,sha256=TkMlUKLrRBWAyeUu-cHKce7TFkwBnWV_Im8DONgnLGU,12852
175
- cartography/intel/aws/resources.py,sha256=R4pDieVCbvvz_gAde606vnxFafKWjE2YfyE8-fv6R7o,4280
176
+ cartography/intel/aws/resources.py,sha256=7jTNcbbodPPrvJrnulgu3KfHCIk692saxfLW1AAXPM8,4343
176
177
  cartography/intel/aws/route53.py,sha256=27ocRlNnxiXi7M7eYLUCVBNwaLyArX4CAExoOBxGl3Y,14294
177
178
  cartography/intel/aws/s3.py,sha256=Da_5NYvQ7MN1AIN7CK9XXlVZo0fPdNHkqBZY1JWnHH4,33598
178
179
  cartography/intel/aws/s3accountpublicaccessblock.py,sha256=XkqHnbj9ODRcc7Rbl4swi03qvw_T-7Bnx3BHpTmlxio,4688
@@ -241,10 +242,11 @@ cartography/intel/duo/phones.py,sha256=3_MPXmR4ub4Jra0ISr6cKzC5z_Pvx7YhHojWeJJku
241
242
  cartography/intel/duo/tokens.py,sha256=lVe0ByS3ncLA3FZXoqN8eLj_HMsl5s4uDT-uWlLJhSs,2407
242
243
  cartography/intel/duo/users.py,sha256=e2eK716wVlO71O9Q9KrWZot2cHjHZ7KgC9ZW27pCDaI,4143
243
244
  cartography/intel/duo/web_authn_credentials.py,sha256=mLWXdBe4V6fHCFotmtJmwhTSoOY6Hx87D3zI3hlL2nc,2656
244
- cartography/intel/entra/__init__.py,sha256=c4zkMFKRem8aRDUqjAvcNw8UwBvAv24Xq8yednlhAjU,2331
245
- cartography/intel/entra/applications.py,sha256=RAvUxk_hwsKbf64ohhg3f-QuTzsT7zb8q13d9nQuyqg,13625
245
+ cartography/intel/entra/__init__.py,sha256=HU0A6XZ413vlSrMRMQzhloxmrr1k6g8ZxEUzb0kVHYw,2944
246
+ cartography/intel/entra/applications.py,sha256=FtZNq7VswfKEflSCqZKDaJ0Hxfs2noOqRsl3QKKtw2c,13550
246
247
  cartography/intel/entra/groups.py,sha256=dENox8CUU_ngHkH0dDgzhH7mBthEovE6Trw6TyNAfGo,5994
247
- cartography/intel/entra/ou.py,sha256=iv5UgRcPIaUZ8DdveKTmMkz0thEPYichpL9XntbHiPo,3803
248
+ cartography/intel/entra/ou.py,sha256=zFX9z0YdISgvHQBTswbEMI2MklrmPkKJyhz5cGC35RA,3786
249
+ cartography/intel/entra/resources.py,sha256=PiMt0-Y_KzNMhmNRkDxP9jmmhxDdmCh5b6PhTUkNZT4,759
248
250
  cartography/intel/entra/users.py,sha256=LiQuvpzPefRWvW1o07l-8TYrr6sUmpiYFli_4ROKJEw,8491
249
251
  cartography/intel/gcp/__init__.py,sha256=K8UOJmrhmG6b3ULJQVmOM4UC-v0KyKMKmhR_07_O41M,19421
250
252
  cartography/intel/gcp/compute.py,sha256=id8B8RqrfyfRITD57KPh4zIHmjrQFSwtH4aNq3XHB2A,48897
@@ -338,8 +340,8 @@ cartography/intel/tailscale/postureintegrations.py,sha256=C75HPfJkWHqlcXjk81YYcS
338
340
  cartography/intel/tailscale/tailnets.py,sha256=Ooc4XSOArWWFJoKykJDrk_tm2OKK1BHwfK_KEhckPp4,1679
339
341
  cartography/intel/tailscale/users.py,sha256=kjHAdIVGUwx28RRQ0KwLsdsZT9L-HlfwTph9dis7grc,1820
340
342
  cartography/intel/tailscale/utils.py,sha256=Xhwcb_31LWQy4MhoFWre_fX8gvddFVt13jjk5p0TPH4,4828
341
- cartography/intel/trivy/__init__.py,sha256=hYroW2ewXRiTPYC7QqxFBaakUgk6j7U66bCSh8cgrGY,5277
342
- cartography/intel/trivy/scanner.py,sha256=u9b5ORpVXaqR6owO6QXjNh9Nz1Vx1nn-rsYfcyrSlx8,12021
343
+ cartography/intel/trivy/__init__.py,sha256=cIWQfCVjl0EKtlod8dzLu543TPrr2XYecmY54TlMioU,7338
344
+ cartography/intel/trivy/scanner.py,sha256=gmWXtBxc_t4iiWp_OrgUkjR8aCZRbGKPAjida2gn58s,12895
343
345
  cartography/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
344
346
  cartography/models/airbyte/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
345
347
  cartography/models/airbyte/connection.py,sha256=_tQSLqOwSU5tplBmLj331Knrv3n_czcvWWWHKpFuirg,5401
@@ -426,6 +428,8 @@ cartography/models/aws/eks/clusters.py,sha256=OthI554MrYNSl-p6ArMJsSH43xKPRDSnEm
426
428
  cartography/models/aws/elasticache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
427
429
  cartography/models/aws/elasticache/cluster.py,sha256=3tx3fL3FPSp4QX3pd42sV71t6kYRl-IfvU_56S7WmGo,3227
428
430
  cartography/models/aws/elasticache/topic.py,sha256=Rq-Hj6xo9xouRLw2NRNU1cmmVUlOP0ieRA8fT5GlZ2w,2757
431
+ cartography/models/aws/eventbridge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
432
+ cartography/models/aws/eventbridge/rule.py,sha256=VH7oTferIyykITCJhVPWZKVx2-7H9Dxi4fm9GwDitJw,3135
429
433
  cartography/models/aws/glue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
430
434
  cartography/models/aws/glue/connection.py,sha256=wG7mDMoYnmAHXrLfi52_1wPHF6VcrLOKfDmZaJJti1Q,2213
431
435
  cartography/models/aws/guardduty/__init__.py,sha256=ILQhP6R9RLgXzPKdmPzuGqhOgAXeIReUEyqKS-ZXS3k,19
@@ -564,7 +568,7 @@ cartography/models/sentinelone/agent.py,sha256=3FTJQQ_QRq9GGeIjuV0_8ZeJcnKWGHHC3
564
568
  cartography/models/sentinelone/application.py,sha256=g77cmjuTmiX_AwgZ7-x5eP-DsKXoCjzeAQuYtuCAcyo,1811
565
569
  cartography/models/sentinelone/application_version.py,sha256=ICxaz1rndwBpaJUgbdTshhn-cbcU_KbV7VeKyUqsdoQ,3829
566
570
  cartography/models/snipeit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
567
- cartography/models/snipeit/asset.py,sha256=64Cq6ff0jemj_fvhQ_-B1xEHqsZ95RqtcbDSTzCI_00,3312
571
+ cartography/models/snipeit/asset.py,sha256=hFvGPH6-6kGgG1yjztkf9wrrAmxNL2p38nc08vow6_w,3371
568
572
  cartography/models/snipeit/tenant.py,sha256=E6uaY3d2W3MmfuUqF2TLpRP3T1QZkoIXRtp9BGxxSxk,695
569
573
  cartography/models/snipeit/user.py,sha256=c-XWAnPTFrFnZLzR_sQB8pKNelwrLjHu-oWQLnQFnxg,2162
570
574
  cartography/models/tailscale/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -578,9 +582,9 @@ cartography/models/trivy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
578
582
  cartography/models/trivy/findings.py,sha256=SgI_h1aRyR20uAHvuXIZ1T6r4IZJt6SVhxRaF2bTsm0,3085
579
583
  cartography/models/trivy/fix.py,sha256=ho9ENVl9HSXqyggyCwR6ilkOBKDxpQ7rGibo_j21NA4,2587
580
584
  cartography/models/trivy/package.py,sha256=IwO1RZZ-MFRtNbt8Cq6YFl6fdNJMFmULnJkkK8Q4rL4,2809
581
- cartography-0.109.0rc2.dist-info/licenses/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
582
- cartography-0.109.0rc2.dist-info/METADATA,sha256=x8x0Hg37Mn7ctpEhcGeHqxPkCZkB7CbuM-3xf3AJHvs,12930
583
- cartography-0.109.0rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
584
- cartography-0.109.0rc2.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
585
- cartography-0.109.0rc2.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
586
- cartography-0.109.0rc2.dist-info/RECORD,,
585
+ cartography-0.110.0rc1.dist-info/licenses/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
586
+ cartography-0.110.0rc1.dist-info/METADATA,sha256=KGMJ4cQnFq-pxGUDUYXa8OqAGvXjQ571C4gbBZiWrMM,12930
587
+ cartography-0.110.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
588
+ cartography-0.110.0rc1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
589
+ cartography-0.110.0rc1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
590
+ cartography-0.110.0rc1.dist-info/RECORD,,