cartography 0.101.0rc1__py3-none-any.whl → 0.101.1__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/data/indexes.cypher +0 -6
- cartography/data/jobs/cleanup/crowdstrike_import_cleanup.json +0 -5
- cartography/data/jobs/scoped_analysis/aws_ec2_iaminstanceprofile.json +15 -0
- cartography/graph/querybuilder.py +9 -1
- cartography/intel/aws/__init__.py +5 -1
- cartography/intel/aws/ec2/launch_templates.py +24 -7
- cartography/intel/aws/ec2/load_balancers.py +126 -148
- cartography/intel/aws/iam_instance_profiles.py +73 -0
- cartography/intel/aws/resources.py +4 -1
- cartography/intel/crowdstrike/__init__.py +17 -5
- cartography/intel/crowdstrike/endpoints.py +12 -44
- cartography/intel/gcp/__init__.py +7 -2
- cartography/intel/gsuite/__init__.py +8 -0
- cartography/intel/kandji/devices.py +27 -3
- cartography/models/aws/ec2/instances.py +17 -0
- cartography/models/aws/ec2/load_balancer_listeners.py +68 -0
- cartography/models/aws/ec2/load_balancers.py +102 -0
- cartography/models/aws/iam/__init__.py +0 -0
- cartography/models/aws/iam/instanceprofile.py +67 -0
- cartography/models/core/common.py +37 -6
- cartography/models/crowdstrike/__init__.py +0 -0
- cartography/models/crowdstrike/hosts.py +49 -0
- cartography/stats.py +1 -1
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/METADATA +4 -3
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/RECORD +30 -24
- cartography/data/jobs/analysis/aws_ec2_iaminstance.json +0 -10
- cartography/data/jobs/analysis/aws_ec2_iaminstanceprofile.json +0 -10
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/WHEEL +0 -0
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/entry_points.txt +0 -0
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/top_level.txt +0 -0
|
@@ -6,6 +6,8 @@ import neo4j
|
|
|
6
6
|
from falconpy.hosts import Hosts
|
|
7
7
|
from falconpy.oauth2 import OAuth2
|
|
8
8
|
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.models.crowdstrike.hosts import CrowdstrikeHostSchema
|
|
9
11
|
from cartography.util import timeit
|
|
10
12
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
@@ -24,55 +26,21 @@ def sync_hosts(
|
|
|
24
26
|
load_host_data(neo4j_session, host_data, update_tag)
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
@timeit
|
|
27
30
|
def load_host_data(
|
|
28
|
-
neo4j_session: neo4j.Session,
|
|
31
|
+
neo4j_session: neo4j.Session,
|
|
32
|
+
data: List[Dict],
|
|
33
|
+
update_tag: int,
|
|
29
34
|
) -> None:
|
|
30
35
|
"""
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
ingestion_cypher_query = """
|
|
34
|
-
UNWIND $Hosts AS host
|
|
35
|
-
MERGE (h:CrowdstrikeHost{id: host.device_id})
|
|
36
|
-
ON CREATE SET h.cid = host.cid,
|
|
37
|
-
h.cid = host.cid,
|
|
38
|
-
h.instance_id = host.instance_id,
|
|
39
|
-
h.firstseen = timestamp()
|
|
40
|
-
SET h.status = host.status,
|
|
41
|
-
h.hostname = host.hostname,
|
|
42
|
-
h.machine_domain = host.machine_domain,
|
|
43
|
-
h.crowdstrike_first_seen = host.first_seen,
|
|
44
|
-
h.crowdstrike_last_seen = host.last_seen,
|
|
45
|
-
h.local_ip = host.local_ip,
|
|
46
|
-
h.external_ip = host.external_ip,
|
|
47
|
-
h.cpu_signature = host.cpu_signature,
|
|
48
|
-
h.bios_manufacturer = host.bios_manufacturer,
|
|
49
|
-
h.bios_version = host.bios_version,
|
|
50
|
-
h.mac_address = host.mac_address,
|
|
51
|
-
h.os_version = host.os_version,
|
|
52
|
-
h.os_build = host.os_build,
|
|
53
|
-
h.platform_id = host.platform_id,
|
|
54
|
-
h.platform_name = host.platform_name,
|
|
55
|
-
h.service_provider = host.service_provider,
|
|
56
|
-
h.service_provider_account_id = host.service_provider_account_id,
|
|
57
|
-
h.agent_version = host.agent_version,
|
|
58
|
-
h.system_manufacturer = host.system_manufacturer,
|
|
59
|
-
h.system_product_name = host.system_product_name,
|
|
60
|
-
h.product_type = host.product_type,
|
|
61
|
-
h.product_type_desc = host.product_type_desc,
|
|
62
|
-
h.provision_status = host.provision_status,
|
|
63
|
-
h.reduced_functionality_mode = host.reduced_functionality_mode,
|
|
64
|
-
h.kernel_version = host.kernel_version,
|
|
65
|
-
h.major_version = host.major_version,
|
|
66
|
-
h.minor_version = host.minor_version,
|
|
67
|
-
h.tags = host.tags,
|
|
68
|
-
h.modified_timestamp = host.modified_timestamp,
|
|
69
|
-
h.lastupdated = $update_tag
|
|
36
|
+
Load Crowdstrike host data into Neo4j.
|
|
70
37
|
"""
|
|
71
38
|
logger.info(f"Loading {len(data)} crowdstrike hosts.")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
39
|
+
load(
|
|
40
|
+
neo4j_session,
|
|
41
|
+
CrowdstrikeHostSchema(),
|
|
42
|
+
data,
|
|
43
|
+
lastupdated=update_tag,
|
|
76
44
|
)
|
|
77
45
|
|
|
78
46
|
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
from collections import namedtuple
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import List
|
|
6
|
+
from typing import Optional
|
|
6
7
|
from typing import Set
|
|
7
8
|
|
|
8
9
|
import googleapiclient.discovery
|
|
@@ -328,7 +329,7 @@ def _sync_multiple_projects(
|
|
|
328
329
|
|
|
329
330
|
|
|
330
331
|
@timeit
|
|
331
|
-
def get_gcp_credentials() -> GoogleCredentials:
|
|
332
|
+
def get_gcp_credentials() -> Optional[GoogleCredentials]:
|
|
332
333
|
"""
|
|
333
334
|
Gets access tokens for GCP API access.
|
|
334
335
|
:param: None
|
|
@@ -338,6 +339,7 @@ def get_gcp_credentials() -> GoogleCredentials:
|
|
|
338
339
|
# Explicitly use Application Default Credentials.
|
|
339
340
|
# See https://google-auth.readthedocs.io/en/master/user-guide.html#application-default-credentials
|
|
340
341
|
credentials, project_id = default()
|
|
342
|
+
return credentials
|
|
341
343
|
except DefaultCredentialsError as e:
|
|
342
344
|
logger.debug("Error occurred calling GoogleCredentials.get_application_default().", exc_info=True)
|
|
343
345
|
logger.error(
|
|
@@ -349,7 +351,7 @@ def get_gcp_credentials() -> GoogleCredentials:
|
|
|
349
351
|
),
|
|
350
352
|
e,
|
|
351
353
|
)
|
|
352
|
-
|
|
354
|
+
return None
|
|
353
355
|
|
|
354
356
|
|
|
355
357
|
@timeit
|
|
@@ -367,6 +369,9 @@ def start_gcp_ingestion(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
credentials = get_gcp_credentials()
|
|
372
|
+
if credentials is None:
|
|
373
|
+
logger.warning("Unable to initialize GCP credentials. Skipping module.")
|
|
374
|
+
return
|
|
370
375
|
|
|
371
376
|
resources = _initialize_resources(credentials)
|
|
372
377
|
|
|
@@ -67,6 +67,14 @@ def start_gsuite_ingestion(neo4j_session: neo4j.Session, config: Config) -> None
|
|
|
67
67
|
|
|
68
68
|
creds: OAuth2Credentials | ServiceAccountCredentials
|
|
69
69
|
if config.gsuite_auth_method == 'delegated': # Legacy delegated method
|
|
70
|
+
if config.gsuite_config is None or not os.path.isfile(config.gsuite_config):
|
|
71
|
+
logger.warning(
|
|
72
|
+
(
|
|
73
|
+
"The GSuite config file is not set or is not a valid file."
|
|
74
|
+
"Skipping GSuite ingestion."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
return
|
|
70
78
|
logger.info('Attempting to authenticate to GSuite using legacy delegated method')
|
|
71
79
|
try:
|
|
72
80
|
creds = service_account.Credentials.from_service_account_file(
|
|
@@ -25,10 +25,34 @@ def get(kandji_base_uri: str, kandji_token: str) -> List[Dict[str, Any]]:
|
|
|
25
25
|
'Authorization': f'Bearer {kandji_token}',
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
offset = 0
|
|
29
|
+
limit = 300
|
|
30
|
+
params: dict[str, str | int] = {
|
|
31
|
+
"sort": "serial_number",
|
|
32
|
+
"limit": limit,
|
|
33
|
+
"offset": offset,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
devices: List[Dict[str, Any]] = []
|
|
28
37
|
session = Session()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
while True:
|
|
39
|
+
logger.debug("Kandji device offset: %s", offset)
|
|
40
|
+
|
|
41
|
+
params["offset"] = offset
|
|
42
|
+
response = session.get(api_endpoint, headers=headers, timeout=_TIMEOUT, params=params)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
|
|
45
|
+
result = response.json()
|
|
46
|
+
# If no more result, we are done
|
|
47
|
+
if len(result) == 0:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
devices.extend(result)
|
|
51
|
+
|
|
52
|
+
offset += limit
|
|
53
|
+
|
|
54
|
+
logger.debug("Kandji device count: %d", len(devices))
|
|
55
|
+
return devices
|
|
32
56
|
|
|
33
57
|
|
|
34
58
|
@timeit
|
|
@@ -71,6 +71,22 @@ class EC2InstanceToEC2Reservation(CartographyRelSchema):
|
|
|
71
71
|
properties: EC2InstanceToEC2ReservationRelProperties = EC2InstanceToEC2ReservationRelProperties()
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class EC2InstanceToInstanceProfileRelProperties(CartographyRelProperties):
|
|
76
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class EC2InstanceToInstanceProfile(CartographyRelSchema):
|
|
81
|
+
target_node_label: str = 'AWSInstanceProfile'
|
|
82
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
83
|
+
{'arn': PropertyRef('IamInstanceProfile')},
|
|
84
|
+
)
|
|
85
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
86
|
+
rel_label: str = "INSTANCE_PROFILE"
|
|
87
|
+
properties: EC2InstanceToInstanceProfileRelProperties = EC2InstanceToInstanceProfileRelProperties()
|
|
88
|
+
|
|
89
|
+
|
|
74
90
|
@dataclass(frozen=True)
|
|
75
91
|
class EC2InstanceSchema(CartographyNodeSchema):
|
|
76
92
|
label: str = 'EC2Instance'
|
|
@@ -79,5 +95,6 @@ class EC2InstanceSchema(CartographyNodeSchema):
|
|
|
79
95
|
other_relationships: OtherRelationships = OtherRelationships(
|
|
80
96
|
[
|
|
81
97
|
EC2InstanceToEC2Reservation(),
|
|
98
|
+
EC2InstanceToInstanceProfile(), # Add the new relationship
|
|
82
99
|
],
|
|
83
100
|
)
|
|
@@ -0,0 +1,68 @@
|
|
|
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.nodes import ExtraNodeLabels
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
8
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
9
|
+
from cartography.models.core.relationships import LinkDirection
|
|
10
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
11
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
12
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ELBListenerNodeProperties(CartographyNodeProperties):
|
|
17
|
+
id: PropertyRef = PropertyRef('id')
|
|
18
|
+
port: PropertyRef = PropertyRef('port')
|
|
19
|
+
protocol: PropertyRef = PropertyRef('protocol')
|
|
20
|
+
instance_port: PropertyRef = PropertyRef('instance_port')
|
|
21
|
+
instance_protocol: PropertyRef = PropertyRef('instance_protocol')
|
|
22
|
+
policy_names: PropertyRef = PropertyRef('policy_names')
|
|
23
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ELBListenerToLoadBalancerRelProperties(CartographyRelProperties):
|
|
28
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class ELBListenerToLoadBalancer(CartographyRelSchema):
|
|
33
|
+
target_node_label: str = 'LoadBalancer'
|
|
34
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
35
|
+
{'id': PropertyRef('LoadBalancerId')},
|
|
36
|
+
)
|
|
37
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
38
|
+
rel_label: str = "ELB_LISTENER"
|
|
39
|
+
properties: ELBListenerToLoadBalancerRelProperties = ELBListenerToLoadBalancerRelProperties()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ELBListenerToAWSAccountRelProperties(CartographyRelProperties):
|
|
44
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class ELBListenerToAWSAccount(CartographyRelSchema):
|
|
49
|
+
target_node_label: str = 'AWSAccount'
|
|
50
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
51
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
52
|
+
)
|
|
53
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
54
|
+
rel_label: str = "RESOURCE"
|
|
55
|
+
properties: ELBListenerToAWSAccountRelProperties = ELBListenerToAWSAccountRelProperties()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ELBListenerSchema(CartographyNodeSchema):
|
|
60
|
+
label: str = 'ELBListener'
|
|
61
|
+
properties: ELBListenerNodeProperties = ELBListenerNodeProperties()
|
|
62
|
+
extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(['Endpoint'])
|
|
63
|
+
sub_resource_relationship: ELBListenerToAWSAccount = ELBListenerToAWSAccount()
|
|
64
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
65
|
+
[
|
|
66
|
+
ELBListenerToLoadBalancer(),
|
|
67
|
+
],
|
|
68
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
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 LoadBalancerNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef('id')
|
|
17
|
+
name: PropertyRef = PropertyRef('name')
|
|
18
|
+
dnsname: PropertyRef = PropertyRef('dnsname', extra_index=True)
|
|
19
|
+
canonicalhostedzonename: PropertyRef = PropertyRef('canonicalhostedzonename')
|
|
20
|
+
canonicalhostedzonenameid: PropertyRef = PropertyRef('canonicalhostedzonenameid')
|
|
21
|
+
scheme: PropertyRef = PropertyRef('scheme')
|
|
22
|
+
region: PropertyRef = PropertyRef('Region', set_in_kwargs=True)
|
|
23
|
+
createdtime: PropertyRef = PropertyRef('createdtime')
|
|
24
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class LoadBalancerToAWSAccountRelProperties(CartographyRelProperties):
|
|
29
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class LoadBalancerToAWSAccount(CartographyRelSchema):
|
|
34
|
+
target_node_label: str = 'AWSAccount'
|
|
35
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
36
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
37
|
+
)
|
|
38
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
39
|
+
rel_label: str = "RESOURCE"
|
|
40
|
+
properties: LoadBalancerToAWSAccountRelProperties = LoadBalancerToAWSAccountRelProperties()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class LoadBalancerToSecurityGroupRelProperties(CartographyRelProperties):
|
|
45
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class LoadBalancerToSourceSecurityGroup(CartographyRelSchema):
|
|
50
|
+
target_node_label: str = 'EC2SecurityGroup'
|
|
51
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
52
|
+
{'name': PropertyRef('GROUP_NAME')},
|
|
53
|
+
)
|
|
54
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
55
|
+
rel_label: str = "SOURCE_SECURITY_GROUP"
|
|
56
|
+
properties: LoadBalancerToSecurityGroupRelProperties = LoadBalancerToSecurityGroupRelProperties()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class LoadBalancerToEC2SecurityGroupRelProperties(CartographyRelProperties):
|
|
61
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class LoadBalancerToEC2SecurityGroup(CartographyRelSchema):
|
|
66
|
+
target_node_label: str = 'EC2SecurityGroup'
|
|
67
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
68
|
+
{'groupid': PropertyRef('GROUP_IDS', one_to_many=True)},
|
|
69
|
+
)
|
|
70
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
71
|
+
rel_label: str = "MEMBER_OF_EC2_SECURITY_GROUP"
|
|
72
|
+
properties: LoadBalancerToEC2SecurityGroupRelProperties = LoadBalancerToEC2SecurityGroupRelProperties()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class LoadBalancerToEC2InstanceRelProperties(CartographyRelProperties):
|
|
77
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class LoadBalancerToEC2Instance(CartographyRelSchema):
|
|
82
|
+
target_node_label: str = 'EC2Instance'
|
|
83
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
84
|
+
{'instanceid': PropertyRef('INSTANCE_IDS', one_to_many=True)},
|
|
85
|
+
)
|
|
86
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
87
|
+
rel_label: str = "EXPOSE"
|
|
88
|
+
properties: LoadBalancerToEC2InstanceRelProperties = LoadBalancerToEC2InstanceRelProperties()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class LoadBalancerSchema(CartographyNodeSchema):
|
|
93
|
+
label: str = 'LoadBalancer'
|
|
94
|
+
properties: LoadBalancerNodeProperties = LoadBalancerNodeProperties()
|
|
95
|
+
sub_resource_relationship: LoadBalancerToAWSAccount = LoadBalancerToAWSAccount()
|
|
96
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
97
|
+
[
|
|
98
|
+
LoadBalancerToSourceSecurityGroup(),
|
|
99
|
+
LoadBalancerToEC2SecurityGroup(),
|
|
100
|
+
LoadBalancerToEC2Instance(),
|
|
101
|
+
],
|
|
102
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
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 InstanceProfileNodeProperties(CartographyNodeProperties):
|
|
16
|
+
"""
|
|
17
|
+
Schema describing a InstanceProfile.
|
|
18
|
+
"""
|
|
19
|
+
arn: PropertyRef = PropertyRef('Arn')
|
|
20
|
+
createdate: PropertyRef = PropertyRef('CreateDate')
|
|
21
|
+
id: PropertyRef = PropertyRef('Arn')
|
|
22
|
+
instance_profile_id: PropertyRef = PropertyRef('InstanceProfileId')
|
|
23
|
+
instance_profile_name: PropertyRef = PropertyRef('InstanceProfileName')
|
|
24
|
+
path: PropertyRef = PropertyRef('Path')
|
|
25
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class InstanceProfileToAwsAccountRelProperties(CartographyRelProperties):
|
|
30
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class InstanceProfileToAWSAccount(CartographyRelSchema):
|
|
35
|
+
target_node_label: str = 'AWSAccount'
|
|
36
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
37
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
38
|
+
)
|
|
39
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
40
|
+
rel_label: str = "RESOURCE"
|
|
41
|
+
properties: InstanceProfileToAwsAccountRelProperties = InstanceProfileToAwsAccountRelProperties()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class InstanceProfileToAWSRoleRelProperties(CartographyRelProperties):
|
|
46
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class InstanceProfileToAWSRole(CartographyRelSchema):
|
|
51
|
+
target_node_label: str = 'AWSRole'
|
|
52
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
53
|
+
{'arn': PropertyRef('Roles', one_to_many=True)},
|
|
54
|
+
)
|
|
55
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
56
|
+
rel_label: str = "ASSOCIATED_WITH"
|
|
57
|
+
properties: InstanceProfileToAWSRoleRelProperties = InstanceProfileToAWSRoleRelProperties()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class InstanceProfileSchema(CartographyNodeSchema):
|
|
62
|
+
label: str = 'AWSInstanceProfile'
|
|
63
|
+
properties: InstanceProfileNodeProperties = InstanceProfileNodeProperties()
|
|
64
|
+
sub_resource_relationship: InstanceProfileToAWSAccount = InstanceProfileToAWSAccount()
|
|
65
|
+
other_relationships: OtherRelationships = OtherRelationships([
|
|
66
|
+
InstanceProfileToAWSRole(),
|
|
67
|
+
])
|
|
@@ -9,12 +9,13 @@ class PropertyRef:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
def __init__(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
set_in_kwargs=False,
|
|
15
|
+
extra_index=False,
|
|
16
|
+
ignore_case=False,
|
|
17
|
+
fuzzy_and_ignore_case=False,
|
|
18
|
+
one_to_many=False,
|
|
18
19
|
):
|
|
19
20
|
"""
|
|
20
21
|
:param name: The name of the property
|
|
@@ -44,19 +45,49 @@ class PropertyRef:
|
|
|
44
45
|
this property using the `CONTAINS` operator.
|
|
45
46
|
query. Defaults to False. This only has effect as part of a TargetNodeMatcher and is not supported for the
|
|
46
47
|
sub resource relationship.
|
|
48
|
+
:param one_to_many: Indicates that this property is meant to create one-to-many associations. If set to True,
|
|
49
|
+
this property ref points to a list stored on the data dict where each item is an ID. Only has effect as
|
|
50
|
+
part of a TargetNodeMatcher and is not supported for the sub resource relationship. Defaults to False.
|
|
51
|
+
Example on why you would set this to True:
|
|
52
|
+
AWS IAM instance profiles can be associated with one or more roles. This is reflected in their API object:
|
|
53
|
+
when we call describe-iam-instance-profiles, the `Roles` field contains a list of all the roles that the
|
|
54
|
+
profile is associated with. So, to create AWSInstanceProfile nodes while attaching them to multiple roles,
|
|
55
|
+
we can create a CartographyRelSchema with
|
|
56
|
+
```
|
|
57
|
+
class InstanceProfileSchema(Schema):
|
|
58
|
+
target_node_label: str = 'AWSRole'
|
|
59
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
60
|
+
'arn': PropertyRef('Roles', one_to_many=True),
|
|
61
|
+
)
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
This means that as we create AWSInstanceProfile nodes, we will search for AWSRoles to attach to, and we do
|
|
65
|
+
this by checking if each role's `arn` field is in the `Roles` list of the data dict.
|
|
47
66
|
"""
|
|
48
67
|
self.name = name
|
|
49
68
|
self.set_in_kwargs = set_in_kwargs
|
|
50
69
|
self.extra_index = extra_index
|
|
51
70
|
self.ignore_case = ignore_case
|
|
52
71
|
self.fuzzy_and_ignore_case = fuzzy_and_ignore_case
|
|
72
|
+
self.one_to_many = one_to_many
|
|
73
|
+
|
|
53
74
|
if self.fuzzy_and_ignore_case and self.ignore_case:
|
|
54
75
|
raise ValueError(
|
|
55
76
|
f'Error setting PropertyRef "{self.name}": ignore_case cannot be used together with'
|
|
56
77
|
'fuzzy_and_ignore_case. Pick one or the other.',
|
|
57
78
|
)
|
|
58
79
|
|
|
80
|
+
if self.one_to_many and (self.ignore_case or self.fuzzy_and_ignore_case):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f'Error setting PropertyRef "{self.name}": one_to_many cannot be used together with '
|
|
83
|
+
'`ignore_case` or `fuzzy_and_ignore_case`.',
|
|
84
|
+
)
|
|
85
|
+
|
|
59
86
|
def _parameterize_name(self) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Prefixes the name of the property ref with a '$' so that we can receive keyword args. See docs on __repr__ for
|
|
89
|
+
PropertyRef.
|
|
90
|
+
"""
|
|
60
91
|
return f"${self.name}"
|
|
61
92
|
|
|
62
93
|
def __repr__(self) -> str:
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
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 CrowdstrikeHostNodeProperties(CartographyNodeProperties):
|
|
10
|
+
id: PropertyRef = PropertyRef('device_id')
|
|
11
|
+
cid: PropertyRef = PropertyRef('cid')
|
|
12
|
+
instance_id: PropertyRef = PropertyRef('instance_id', extra_index=True)
|
|
13
|
+
serial_number: PropertyRef = PropertyRef('serial_number', extra_index=True)
|
|
14
|
+
status: PropertyRef = PropertyRef('status')
|
|
15
|
+
hostname: PropertyRef = PropertyRef('hostname')
|
|
16
|
+
machine_domain: PropertyRef = PropertyRef('machine_domain')
|
|
17
|
+
crowdstrike_first_seen: PropertyRef = PropertyRef('first_seen')
|
|
18
|
+
crowdstrike_last_seen: PropertyRef = PropertyRef('last_seen')
|
|
19
|
+
local_ip: PropertyRef = PropertyRef('local_ip')
|
|
20
|
+
external_ip: PropertyRef = PropertyRef('external_ip')
|
|
21
|
+
cpu_signature: PropertyRef = PropertyRef('cpu_signature')
|
|
22
|
+
bios_manufacturer: PropertyRef = PropertyRef('bios_manufacturer')
|
|
23
|
+
bios_version: PropertyRef = PropertyRef('bios_version')
|
|
24
|
+
mac_address: PropertyRef = PropertyRef('mac_address')
|
|
25
|
+
os_version: PropertyRef = PropertyRef('os_version')
|
|
26
|
+
os_build: PropertyRef = PropertyRef('os_build')
|
|
27
|
+
platform_id: PropertyRef = PropertyRef('platform_id')
|
|
28
|
+
platform_name: PropertyRef = PropertyRef('platform_name')
|
|
29
|
+
service_provider: PropertyRef = PropertyRef('service_provider')
|
|
30
|
+
service_provider_account_id: PropertyRef = PropertyRef('service_provider_account_id')
|
|
31
|
+
agent_version: PropertyRef = PropertyRef('agent_version')
|
|
32
|
+
system_manufacturer: PropertyRef = PropertyRef('system_manufacturer')
|
|
33
|
+
system_product_name: PropertyRef = PropertyRef('system_product_name')
|
|
34
|
+
product_type: PropertyRef = PropertyRef('product_type')
|
|
35
|
+
product_type_desc: PropertyRef = PropertyRef('product_type_desc')
|
|
36
|
+
provision_status: PropertyRef = PropertyRef('provision_status')
|
|
37
|
+
reduced_functionality_mode: PropertyRef = PropertyRef('reduced_functionality_mode')
|
|
38
|
+
kernel_version: PropertyRef = PropertyRef('kernel_version')
|
|
39
|
+
major_version: PropertyRef = PropertyRef('major_version')
|
|
40
|
+
minor_version: PropertyRef = PropertyRef('minor_version')
|
|
41
|
+
tags: PropertyRef = PropertyRef('tags')
|
|
42
|
+
modified_timestamp: PropertyRef = PropertyRef('modified_timestamp')
|
|
43
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class CrowdstrikeHostSchema(CartographyNodeSchema):
|
|
48
|
+
label: str = 'CrowdstrikeHost'
|
|
49
|
+
properties: CrowdstrikeHostNodeProperties = CrowdstrikeHostNodeProperties()
|
cartography/stats.py
CHANGED
|
@@ -97,7 +97,7 @@ def set_stats_client(stats_client: StatsClient) -> None:
|
|
|
97
97
|
"""
|
|
98
98
|
This is used to set the module level stats client configured to talk with a statsd host
|
|
99
99
|
"""
|
|
100
|
-
global _scoped_stats_client
|
|
100
|
+
global _scoped_stats_client # noqa: F824
|
|
101
101
|
_scoped_stats_client.set_stats_client(stats_client)
|
|
102
102
|
|
|
103
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.101.
|
|
3
|
+
Version: 0.101.1
|
|
4
4
|
Summary: Explore assets and their relationships across your technical infrastructure.
|
|
5
5
|
Maintainer: Cartography Contributors
|
|
6
6
|
License: apache2
|
|
@@ -59,10 +59,10 @@ Requires-Dist: moto; extra == "dev"
|
|
|
59
59
|
Requires-Dist: pre-commit; extra == "dev"
|
|
60
60
|
Requires-Dist: pytest>=6.2.4; extra == "dev"
|
|
61
61
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
62
|
-
Requires-Dist: pytest-cov==6.
|
|
62
|
+
Requires-Dist: pytest-cov==6.1.1; extra == "dev"
|
|
63
63
|
Requires-Dist: pytest-rerunfailures; extra == "dev"
|
|
64
64
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
65
|
-
Requires-Dist: types-requests<2.32.0.
|
|
65
|
+
Requires-Dist: types-requests<2.32.0.20250329; extra == "dev"
|
|
66
66
|
Dynamic: license-file
|
|
67
67
|
|
|
68
68
|

|
|
@@ -182,6 +182,7 @@ Get started with our [developer documentation](https://cartography-cncf.github.i
|
|
|
182
182
|
1. [MessageBird](https://messagebird.com)
|
|
183
183
|
1. [Cloudanix](https://www.cloudanix.com/)
|
|
184
184
|
1. [Corelight](https://www.corelight.com/)
|
|
185
|
+
1. [SubImage](https://subimage.io)
|
|
185
186
|
1. {Your company here} :-)
|
|
186
187
|
|
|
187
188
|
If your organization uses Cartography, please file a PR and update this list. Say hi on Slack too!
|