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 +2 -2
- cartography/cli.py +22 -0
- cartography/config.py +13 -0
- cartography/intel/aws/cloudtrail_management_events.py +21 -0
- cartography/intel/aws/eventbridge.py +91 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/entra/__init__.py +43 -41
- cartography/intel/entra/applications.py +1 -2
- cartography/intel/entra/ou.py +1 -1
- cartography/intel/entra/resources.py +20 -0
- cartography/intel/trivy/__init__.py +73 -13
- cartography/intel/trivy/scanner.py +115 -92
- cartography/models/aws/eventbridge/__init__.py +0 -0
- cartography/models/aws/eventbridge/rule.py +77 -0
- cartography/models/snipeit/asset.py +1 -0
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/METADATA +1 -1
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/RECORD +21 -17
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/WHEEL +0 -0
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.109.0rc2.dist-info → cartography-0.110.0rc1.dist-info}/top_level.txt +0 -0
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
179
|
+
raise
|
|
181
180
|
|
|
182
181
|
logger.info(f"Retrieved {len(assignments)} app role assignments total")
|
|
183
182
|
return assignments
|
cartography/intel/entra/ou.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
185
|
+
config: Configuration object containing S3 or directory paths
|
|
135
186
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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,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=
|
|
4
|
-
cartography/cli.py,sha256=
|
|
5
|
-
cartography/config.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
245
|
-
cartography/intel/entra/applications.py,sha256=
|
|
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=
|
|
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=
|
|
342
|
-
cartography/intel/trivy/scanner.py,sha256=
|
|
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=
|
|
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.
|
|
582
|
-
cartography-0.
|
|
583
|
-
cartography-0.
|
|
584
|
-
cartography-0.
|
|
585
|
-
cartography-0.
|
|
586
|
-
cartography-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|