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.

Files changed (32) hide show
  1. cartography/_version.py +2 -2
  2. cartography/data/indexes.cypher +0 -6
  3. cartography/data/jobs/cleanup/crowdstrike_import_cleanup.json +0 -5
  4. cartography/data/jobs/scoped_analysis/aws_ec2_iaminstanceprofile.json +15 -0
  5. cartography/graph/querybuilder.py +9 -1
  6. cartography/intel/aws/__init__.py +5 -1
  7. cartography/intel/aws/ec2/launch_templates.py +24 -7
  8. cartography/intel/aws/ec2/load_balancers.py +126 -148
  9. cartography/intel/aws/iam_instance_profiles.py +73 -0
  10. cartography/intel/aws/resources.py +4 -1
  11. cartography/intel/crowdstrike/__init__.py +17 -5
  12. cartography/intel/crowdstrike/endpoints.py +12 -44
  13. cartography/intel/gcp/__init__.py +7 -2
  14. cartography/intel/gsuite/__init__.py +8 -0
  15. cartography/intel/kandji/devices.py +27 -3
  16. cartography/models/aws/ec2/instances.py +17 -0
  17. cartography/models/aws/ec2/load_balancer_listeners.py +68 -0
  18. cartography/models/aws/ec2/load_balancers.py +102 -0
  19. cartography/models/aws/iam/__init__.py +0 -0
  20. cartography/models/aws/iam/instanceprofile.py +67 -0
  21. cartography/models/core/common.py +37 -6
  22. cartography/models/crowdstrike/__init__.py +0 -0
  23. cartography/models/crowdstrike/hosts.py +49 -0
  24. cartography/stats.py +1 -1
  25. {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/METADATA +4 -3
  26. {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/RECORD +30 -24
  27. cartography/data/jobs/analysis/aws_ec2_iaminstance.json +0 -10
  28. cartography/data/jobs/analysis/aws_ec2_iaminstanceprofile.json +0 -10
  29. {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/WHEEL +0 -0
  30. {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/entry_points.txt +0 -0
  31. {cartography-0.101.0rc1.dist-info → cartography-0.101.1.dist-info}/licenses/LICENSE +0 -0
  32. {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, data: List[Dict], update_tag: int,
31
+ neo4j_session: neo4j.Session,
32
+ data: List[Dict],
33
+ update_tag: int,
29
34
  ) -> None:
30
35
  """
31
- Transform and load scan information
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
- neo4j_session.run(
73
- ingestion_cypher_query,
74
- Hosts=data,
75
- update_tag=update_tag,
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
- return credentials
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
- req = session.get(api_endpoint, headers=headers, timeout=_TIMEOUT)
30
- req.raise_for_status()
31
- return req.json()
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
- self,
13
- name: str,
14
- set_in_kwargs=False,
15
- extra_index=False,
16
- ignore_case=False,
17
- fuzzy_and_ignore_case=False,
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.0rc1
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.0.0; extra == "dev"
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.20250307; extra == "dev"
65
+ Requires-Dist: types-requests<2.32.0.20250329; extra == "dev"
66
66
  Dynamic: license-file
67
67
 
68
68
  ![Cartography](docs/root/images/logo-horizontal.png)
@@ -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!