cartography 0.102.0rc1__py3-none-any.whl → 0.103.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/__main__.py +1 -2
- cartography/_version.py +2 -2
- cartography/cli.py +302 -253
- cartography/client/core/tx.py +39 -18
- cartography/config.py +4 -0
- cartography/driftdetect/__main__.py +1 -2
- cartography/driftdetect/add_shortcut.py +10 -2
- cartography/driftdetect/cli.py +71 -75
- cartography/driftdetect/detect_deviations.py +7 -3
- cartography/driftdetect/get_states.py +20 -8
- cartography/driftdetect/model.py +5 -5
- cartography/driftdetect/serializers.py +8 -6
- cartography/driftdetect/storage.py +2 -2
- cartography/graph/cleanupbuilder.py +35 -15
- cartography/graph/job.py +46 -17
- cartography/graph/querybuilder.py +165 -80
- cartography/graph/statement.py +35 -26
- cartography/intel/analysis.py +4 -1
- cartography/intel/aws/__init__.py +114 -55
- cartography/intel/aws/apigateway.py +134 -63
- cartography/intel/aws/cloudtrail.py +127 -0
- cartography/intel/aws/config.py +56 -20
- cartography/intel/aws/dynamodb.py +108 -40
- cartography/intel/aws/ec2/__init__.py +2 -2
- cartography/intel/aws/ec2/auto_scaling_groups.py +181 -78
- cartography/intel/aws/ec2/elastic_ip_addresses.py +41 -13
- cartography/intel/aws/ec2/images.py +49 -20
- cartography/intel/aws/ec2/instances.py +234 -136
- cartography/intel/aws/ec2/internet_gateways.py +40 -11
- cartography/intel/aws/ec2/key_pairs.py +44 -20
- cartography/intel/aws/ec2/launch_templates.py +101 -59
- cartography/intel/aws/ec2/load_balancer_v2s.py +104 -39
- cartography/intel/aws/ec2/load_balancers.py +82 -42
- cartography/intel/aws/ec2/network_acls.py +89 -65
- cartography/intel/aws/ec2/network_interfaces.py +146 -87
- cartography/intel/aws/ec2/reserved_instances.py +45 -16
- cartography/intel/aws/ec2/route_tables.py +327 -0
- cartography/intel/aws/ec2/security_groups.py +71 -21
- cartography/intel/aws/ec2/snapshots.py +61 -22
- cartography/intel/aws/ec2/subnets.py +54 -18
- cartography/intel/aws/ec2/tgw.py +100 -34
- cartography/intel/aws/ec2/util.py +1 -1
- cartography/intel/aws/ec2/volumes.py +69 -41
- cartography/intel/aws/ec2/vpc.py +37 -12
- cartography/intel/aws/ec2/vpc_peerings.py +83 -24
- cartography/intel/aws/ecr.py +88 -32
- cartography/intel/aws/ecs.py +83 -47
- cartography/intel/aws/eks.py +55 -29
- cartography/intel/aws/elasticache.py +42 -18
- cartography/intel/aws/elasticsearch.py +57 -20
- cartography/intel/aws/emr.py +61 -23
- cartography/intel/aws/iam.py +401 -145
- cartography/intel/aws/iam_instance_profiles.py +22 -22
- cartography/intel/aws/identitycenter.py +71 -37
- cartography/intel/aws/inspector.py +159 -89
- cartography/intel/aws/kms.py +92 -38
- cartography/intel/aws/lambda_function.py +103 -34
- cartography/intel/aws/organizations.py +30 -10
- cartography/intel/aws/permission_relationships.py +133 -51
- cartography/intel/aws/rds.py +249 -85
- cartography/intel/aws/redshift.py +107 -46
- cartography/intel/aws/resourcegroupstaggingapi.py +120 -66
- cartography/intel/aws/resources.py +53 -44
- cartography/intel/aws/route53.py +108 -61
- cartography/intel/aws/s3.py +168 -83
- cartography/intel/aws/s3accountpublicaccessblock.py +157 -0
- cartography/intel/aws/secretsmanager.py +24 -12
- cartography/intel/aws/securityhub.py +20 -9
- cartography/intel/aws/sns.py +166 -0
- cartography/intel/aws/sqs.py +60 -28
- cartography/intel/aws/ssm.py +70 -30
- cartography/intel/aws/util/arns.py +7 -7
- cartography/intel/aws/util/common.py +31 -4
- cartography/intel/azure/__init__.py +78 -19
- cartography/intel/azure/compute.py +101 -27
- cartography/intel/azure/cosmosdb.py +496 -170
- cartography/intel/azure/sql.py +296 -105
- cartography/intel/azure/storage.py +322 -113
- cartography/intel/azure/subscription.py +39 -23
- cartography/intel/azure/tenant.py +13 -4
- cartography/intel/azure/util/credentials.py +95 -55
- cartography/intel/bigfix/__init__.py +2 -2
- cartography/intel/bigfix/computers.py +93 -65
- cartography/intel/create_indexes.py +3 -2
- cartography/intel/crowdstrike/__init__.py +11 -9
- cartography/intel/crowdstrike/endpoints.py +5 -1
- cartography/intel/crowdstrike/spotlight.py +8 -3
- cartography/intel/cve/__init__.py +46 -13
- cartography/intel/cve/feed.py +48 -12
- cartography/intel/digitalocean/__init__.py +22 -13
- cartography/intel/digitalocean/compute.py +75 -108
- cartography/intel/digitalocean/management.py +44 -80
- cartography/intel/digitalocean/platform.py +48 -43
- cartography/intel/dns.py +36 -10
- cartography/intel/duo/__init__.py +21 -16
- cartography/intel/duo/api_host.py +14 -9
- cartography/intel/duo/endpoints.py +50 -45
- cartography/intel/duo/groups.py +18 -14
- cartography/intel/duo/phones.py +37 -34
- cartography/intel/duo/tokens.py +26 -23
- cartography/intel/duo/users.py +54 -50
- cartography/intel/duo/web_authn_credentials.py +30 -25
- cartography/intel/entra/__init__.py +25 -7
- cartography/intel/entra/ou.py +112 -0
- cartography/intel/entra/users.py +69 -63
- cartography/intel/gcp/__init__.py +185 -49
- cartography/intel/gcp/compute.py +418 -231
- cartography/intel/gcp/crm.py +96 -43
- cartography/intel/gcp/dns.py +60 -19
- cartography/intel/gcp/gke.py +72 -38
- cartography/intel/gcp/iam.py +61 -41
- cartography/intel/gcp/storage.py +84 -55
- cartography/intel/github/__init__.py +13 -11
- cartography/intel/github/repos.py +270 -137
- cartography/intel/github/teams.py +170 -88
- cartography/intel/github/users.py +70 -39
- cartography/intel/github/util.py +36 -34
- cartography/intel/gsuite/__init__.py +47 -26
- cartography/intel/gsuite/api.py +73 -30
- cartography/intel/jamf/__init__.py +19 -1
- cartography/intel/jamf/computers.py +30 -7
- cartography/intel/jamf/util.py +7 -2
- cartography/intel/kandji/__init__.py +6 -3
- cartography/intel/kandji/devices.py +14 -8
- cartography/intel/kubernetes/namespaces.py +7 -4
- cartography/intel/kubernetes/pods.py +7 -4
- cartography/intel/kubernetes/services.py +8 -4
- cartography/intel/lastpass/__init__.py +2 -2
- cartography/intel/lastpass/users.py +23 -12
- cartography/intel/oci/__init__.py +44 -11
- cartography/intel/oci/iam.py +134 -38
- cartography/intel/oci/organizations.py +13 -6
- cartography/intel/oci/utils.py +43 -20
- cartography/intel/okta/__init__.py +66 -15
- cartography/intel/okta/applications.py +42 -20
- cartography/intel/okta/awssaml.py +93 -33
- cartography/intel/okta/factors.py +16 -4
- cartography/intel/okta/groups.py +56 -29
- cartography/intel/okta/organization.py +5 -1
- cartography/intel/okta/origins.py +6 -2
- cartography/intel/okta/roles.py +15 -5
- cartography/intel/okta/users.py +20 -8
- cartography/intel/okta/utils.py +6 -4
- cartography/intel/pagerduty/__init__.py +8 -7
- cartography/intel/pagerduty/escalation_policies.py +18 -6
- cartography/intel/pagerduty/schedules.py +12 -4
- cartography/intel/pagerduty/services.py +11 -4
- cartography/intel/pagerduty/teams.py +8 -3
- cartography/intel/pagerduty/users.py +3 -1
- cartography/intel/pagerduty/vendors.py +3 -1
- cartography/intel/semgrep/__init__.py +24 -6
- cartography/intel/semgrep/dependencies.py +50 -28
- cartography/intel/semgrep/deployment.py +3 -1
- cartography/intel/semgrep/findings.py +42 -18
- cartography/intel/snipeit/__init__.py +17 -3
- cartography/intel/snipeit/asset.py +12 -6
- cartography/intel/snipeit/user.py +8 -5
- cartography/intel/snipeit/util.py +9 -4
- cartography/models/aws/apigateway.py +21 -17
- cartography/models/aws/apigatewaycertificate.py +28 -22
- cartography/models/aws/apigatewayresource.py +28 -20
- cartography/models/aws/apigatewaystage.py +33 -25
- cartography/models/aws/cloudtrail/__init__.py +0 -0
- cartography/models/aws/cloudtrail/trail.py +61 -0
- cartography/models/aws/dynamodb/gsi.py +30 -22
- cartography/models/aws/dynamodb/tables.py +25 -17
- cartography/models/aws/ec2/auto_scaling_groups.py +102 -82
- cartography/models/aws/ec2/images.py +36 -34
- cartography/models/aws/ec2/instances.py +51 -45
- cartography/models/aws/ec2/keypair.py +21 -16
- cartography/models/aws/ec2/keypair_instance.py +28 -21
- cartography/models/aws/ec2/launch_configurations.py +30 -26
- cartography/models/aws/ec2/launch_template_versions.py +48 -38
- cartography/models/aws/ec2/launch_templates.py +21 -17
- cartography/models/aws/ec2/load_balancer_listeners.py +27 -23
- cartography/models/aws/ec2/load_balancers.py +47 -37
- cartography/models/aws/ec2/network_acl_rules.py +38 -30
- cartography/models/aws/ec2/network_acls.py +38 -29
- cartography/models/aws/ec2/networkinterface_instance.py +52 -39
- cartography/models/aws/ec2/networkinterfaces.py +53 -37
- cartography/models/aws/ec2/privateip_networkinterface.py +32 -22
- cartography/models/aws/ec2/reservations.py +18 -14
- cartography/models/aws/ec2/route_table_associations.py +97 -0
- cartography/models/aws/ec2/route_tables.py +128 -0
- cartography/models/aws/ec2/routes.py +85 -0
- cartography/models/aws/ec2/securitygroup_instance.py +29 -20
- cartography/models/aws/ec2/securitygroup_networkinterface.py +24 -15
- cartography/models/aws/ec2/subnet_instance.py +24 -19
- cartography/models/aws/ec2/subnet_networkinterface.py +40 -31
- cartography/models/aws/ec2/volumes.py +47 -40
- cartography/models/aws/eks/clusters.py +23 -21
- cartography/models/aws/emr.py +32 -30
- cartography/models/aws/iam/instanceprofile.py +33 -24
- cartography/models/aws/identitycenter/awsidentitycenter.py +18 -14
- cartography/models/aws/identitycenter/awspermissionset.py +37 -29
- cartography/models/aws/identitycenter/awsssouser.py +23 -21
- cartography/models/aws/inspector/findings.py +77 -65
- cartography/models/aws/inspector/packages.py +35 -29
- cartography/models/aws/s3/__init__.py +0 -0
- cartography/models/aws/s3/account_public_access_block.py +51 -0
- cartography/models/aws/sns/__init__.py +0 -0
- cartography/models/aws/sns/topic.py +50 -0
- cartography/models/aws/ssm/instance_information.py +51 -39
- cartography/models/aws/ssm/instance_patch.py +32 -26
- cartography/models/bigfix/bigfix_computer.py +42 -38
- cartography/models/bigfix/bigfix_root.py +3 -3
- cartography/models/core/common.py +12 -10
- cartography/models/core/nodes.py +5 -2
- cartography/models/core/relationships.py +14 -6
- cartography/models/crowdstrike/hosts.py +37 -35
- cartography/models/cve/cve.py +34 -32
- cartography/models/cve/cve_feed.py +6 -6
- cartography/models/digitalocean/__init__.py +0 -0
- cartography/models/digitalocean/account.py +21 -0
- cartography/models/digitalocean/droplet.py +56 -0
- cartography/models/digitalocean/project.py +48 -0
- cartography/models/duo/api_host.py +3 -3
- cartography/models/duo/endpoint.py +43 -41
- cartography/models/duo/group.py +14 -14
- cartography/models/duo/phone.py +27 -27
- cartography/models/duo/token.py +16 -16
- cartography/models/duo/user.py +46 -44
- cartography/models/duo/web_authn_credential.py +27 -19
- cartography/models/entra/ou.py +48 -0
- cartography/models/entra/tenant.py +24 -18
- cartography/models/entra/user.py +64 -48
- cartography/models/gcp/iam.py +23 -23
- cartography/models/github/orgs.py +5 -4
- cartography/models/github/teams.py +37 -31
- cartography/models/github/users.py +34 -23
- cartography/models/kandji/device.py +22 -16
- cartography/models/kandji/tenant.py +6 -4
- cartography/models/lastpass/tenant.py +3 -3
- cartography/models/lastpass/user.py +32 -28
- cartography/models/semgrep/dependencies.py +36 -24
- cartography/models/semgrep/deployment.py +5 -5
- cartography/models/semgrep/findings.py +58 -42
- cartography/models/semgrep/locations.py +27 -21
- cartography/models/snipeit/asset.py +30 -21
- cartography/models/snipeit/tenant.py +6 -4
- cartography/models/snipeit/user.py +19 -12
- cartography/stats.py +3 -3
- cartography/sync.py +107 -31
- cartography/util.py +84 -62
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0rc1.dist-info}/METADATA +3 -14
- cartography-0.103.0rc1.dist-info/RECORD +396 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0rc1.dist-info}/WHEEL +1 -1
- cartography-0.102.0rc1.dist-info/RECORD +0 -377
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0rc1.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.102.0rc1.dist-info → cartography-0.103.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -20,8 +20,8 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _build_node_properties_statement(
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
node_property_map: Dict[str, PropertyRef],
|
|
24
|
+
extra_node_labels: Optional[ExtraNodeLabels] = None,
|
|
25
25
|
) -> str:
|
|
26
26
|
"""
|
|
27
27
|
Generate a Neo4j clause that sets node properties using the given mapping of attribute names to PropertyRefs.
|
|
@@ -46,22 +46,31 @@ def _build_node_properties_statement(
|
|
|
46
46
|
:param extra_node_labels: Optional ExtraNodeLabels object to set on the node as string
|
|
47
47
|
:return: The resulting Neo4j SET clause to set the given attributes on the node
|
|
48
48
|
"""
|
|
49
|
-
ingest_fields_template = Template(
|
|
49
|
+
ingest_fields_template = Template("i.$node_property = $property_ref")
|
|
50
50
|
|
|
51
|
-
set_clause =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
set_clause = ",\n".join(
|
|
52
|
+
[
|
|
53
|
+
ingest_fields_template.safe_substitute(
|
|
54
|
+
node_property=node_property,
|
|
55
|
+
property_ref=property_ref,
|
|
56
|
+
)
|
|
57
|
+
for node_property, property_ref in node_property_map.items()
|
|
58
|
+
if node_property
|
|
59
|
+
!= "id" # The `MERGE` clause will have already set `id`; let's not set it again.
|
|
60
|
+
],
|
|
61
|
+
)
|
|
56
62
|
|
|
57
63
|
# Set extra labels on the node if specified
|
|
58
64
|
if extra_node_labels:
|
|
59
|
-
extra_labels =
|
|
65
|
+
extra_labels = ":".join([label for label in extra_node_labels.labels])
|
|
60
66
|
set_clause += f",\n i:{extra_labels}"
|
|
61
67
|
return set_clause
|
|
62
68
|
|
|
63
69
|
|
|
64
|
-
def _build_rel_properties_statement(
|
|
70
|
+
def _build_rel_properties_statement(
|
|
71
|
+
rel_var: str,
|
|
72
|
+
rel_property_map: Optional[Dict[str, PropertyRef]] = None,
|
|
73
|
+
) -> str:
|
|
65
74
|
"""
|
|
66
75
|
Generate a Neo4j clause that sets relationship properties using the given mapping of attribute names to
|
|
67
76
|
PropertyRefs.
|
|
@@ -83,18 +92,20 @@ def _build_rel_properties_statement(rel_var: str, rel_property_map: Optional[Dic
|
|
|
83
92
|
:param rel_property_map: Mapping of relationship attribute names as str to PropertyRef objects
|
|
84
93
|
:return: The resulting Neo4j SET clause to set the given attributes on the relationship
|
|
85
94
|
"""
|
|
86
|
-
set_clause =
|
|
87
|
-
ingest_fields_template = Template(
|
|
95
|
+
set_clause = ""
|
|
96
|
+
ingest_fields_template = Template("$rel_var.$rel_property = $property_ref")
|
|
88
97
|
|
|
89
98
|
if rel_property_map:
|
|
90
|
-
set_clause +=
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
set_clause += ",\n".join(
|
|
100
|
+
[
|
|
101
|
+
ingest_fields_template.safe_substitute(
|
|
102
|
+
rel_var=rel_var,
|
|
103
|
+
rel_property=rel_property,
|
|
104
|
+
property_ref=property_ref,
|
|
105
|
+
)
|
|
106
|
+
for rel_property, property_ref in rel_property_map.items()
|
|
107
|
+
],
|
|
108
|
+
)
|
|
98
109
|
return set_clause
|
|
99
110
|
|
|
100
111
|
|
|
@@ -106,12 +117,15 @@ def _build_match_clause(matcher: TargetNodeMatcher) -> str:
|
|
|
106
117
|
"""
|
|
107
118
|
match = Template("$Key: $PropRef")
|
|
108
119
|
matcher_asdict = asdict(matcher)
|
|
109
|
-
return
|
|
120
|
+
return ", ".join(
|
|
121
|
+
match.safe_substitute(Key=key, PropRef=prop_ref)
|
|
122
|
+
for key, prop_ref in matcher_asdict.items()
|
|
123
|
+
)
|
|
110
124
|
|
|
111
125
|
|
|
112
126
|
def _build_where_clause_for_rel_match(
|
|
113
|
-
|
|
114
|
-
|
|
127
|
+
node_var: str,
|
|
128
|
+
matcher: TargetNodeMatcher,
|
|
115
129
|
) -> str:
|
|
116
130
|
"""
|
|
117
131
|
Same as _build_match_clause, but puts the matching logic in a WHERE clause.
|
|
@@ -121,7 +135,9 @@ def _build_where_clause_for_rel_match(
|
|
|
121
135
|
"""
|
|
122
136
|
match = Template("$node_var.$key = $prop_ref")
|
|
123
137
|
case_insensitive_match = Template("toLower($node_var.$key) = toLower($prop_ref)")
|
|
124
|
-
fuzzy_and_ignorecase_match = Template(
|
|
138
|
+
fuzzy_and_ignorecase_match = Template(
|
|
139
|
+
"toLower($node_var.$key) CONTAINS toLower($prop_ref)"
|
|
140
|
+
)
|
|
125
141
|
# This assumes that item.$prop_ref points to a list available on the data object
|
|
126
142
|
one_to_many_match = Template("$node_var.$key IN $prop_ref")
|
|
127
143
|
|
|
@@ -130,20 +146,34 @@ def _build_where_clause_for_rel_match(
|
|
|
130
146
|
result = []
|
|
131
147
|
for key, prop_ref in matcher_asdict.items():
|
|
132
148
|
if prop_ref.ignore_case:
|
|
133
|
-
prop_line = case_insensitive_match.safe_substitute(
|
|
149
|
+
prop_line = case_insensitive_match.safe_substitute(
|
|
150
|
+
node_var=node_var,
|
|
151
|
+
key=key,
|
|
152
|
+
prop_ref=prop_ref,
|
|
153
|
+
)
|
|
134
154
|
elif prop_ref.fuzzy_and_ignore_case:
|
|
135
|
-
prop_line = fuzzy_and_ignorecase_match.safe_substitute(
|
|
155
|
+
prop_line = fuzzy_and_ignorecase_match.safe_substitute(
|
|
156
|
+
node_var=node_var, key=key, prop_ref=prop_ref
|
|
157
|
+
)
|
|
136
158
|
elif prop_ref.one_to_many:
|
|
137
159
|
# Allow a single node to be attached to multiple others at once using a list of IDs provided in kwargs
|
|
138
|
-
prop_line = one_to_many_match.safe_substitute(
|
|
160
|
+
prop_line = one_to_many_match.safe_substitute(
|
|
161
|
+
node_var=node_var, key=key, prop_ref=prop_ref
|
|
162
|
+
)
|
|
139
163
|
else:
|
|
140
164
|
# Exact match (default; most efficient)
|
|
141
|
-
prop_line = match.safe_substitute(
|
|
165
|
+
prop_line = match.safe_substitute(
|
|
166
|
+
node_var=node_var,
|
|
167
|
+
key=key,
|
|
168
|
+
prop_ref=prop_ref,
|
|
169
|
+
)
|
|
142
170
|
result.append(prop_line)
|
|
143
|
-
return
|
|
171
|
+
return " AND\n".join(result)
|
|
144
172
|
|
|
145
173
|
|
|
146
|
-
def _asdict_with_validate_relprops(
|
|
174
|
+
def _asdict_with_validate_relprops(
|
|
175
|
+
link: CartographyRelSchema,
|
|
176
|
+
) -> Dict[str, PropertyRef]:
|
|
147
177
|
"""
|
|
148
178
|
Give a helpful error message when forgetting to put `()` when instantiating a CartographyRelSchema, as this
|
|
149
179
|
isn't always caught by IDEs.
|
|
@@ -151,18 +181,24 @@ def _asdict_with_validate_relprops(link: CartographyRelSchema) -> Dict[str, Prop
|
|
|
151
181
|
try:
|
|
152
182
|
rel_props_as_dict: Dict[str, PropertyRef] = asdict(link.properties)
|
|
153
183
|
except TypeError as e:
|
|
154
|
-
if
|
|
184
|
+
if (
|
|
185
|
+
e.args
|
|
186
|
+
and e.args[0]
|
|
187
|
+
and e.args == "asdict() should be called on dataclass instances"
|
|
188
|
+
):
|
|
155
189
|
logger.error(
|
|
156
190
|
f'TypeError thrown when trying to draw relation "{link.rel_label}" to a "{link.target_node_label}" '
|
|
157
|
-
f
|
|
158
|
-
f
|
|
159
|
-
f
|
|
191
|
+
f"node. Please make sure that you did not forget to write `()` when specifying `properties` in the"
|
|
192
|
+
f"dataclass. "
|
|
193
|
+
f"For example, do `properties: RelProp = RelProp()`; NOT `properties: RelProp = RelProp`.",
|
|
160
194
|
)
|
|
161
195
|
raise
|
|
162
196
|
return rel_props_as_dict
|
|
163
197
|
|
|
164
198
|
|
|
165
|
-
def _build_attach_sub_resource_statement(
|
|
199
|
+
def _build_attach_sub_resource_statement(
|
|
200
|
+
sub_resource_link: Optional[CartographyRelSchema] = None,
|
|
201
|
+
) -> str:
|
|
166
202
|
"""
|
|
167
203
|
Generates a Neo4j statement to attach a sub resource to a node. A 'sub resource' is a term we made up to describe
|
|
168
204
|
billing units of a given resource. For example,
|
|
@@ -176,7 +212,7 @@ def _build_attach_sub_resource_statement(sub_resource_link: Optional[Cartography
|
|
|
176
212
|
keys, and directionality. If sub_resource_link is None, return an empty string.
|
|
177
213
|
"""
|
|
178
214
|
if not sub_resource_link:
|
|
179
|
-
return
|
|
215
|
+
return ""
|
|
180
216
|
|
|
181
217
|
sub_resource_attach_template = Template(
|
|
182
218
|
"""
|
|
@@ -195,22 +231,29 @@ def _build_attach_sub_resource_statement(sub_resource_link: Optional[Cartography
|
|
|
195
231
|
else:
|
|
196
232
|
rel_merge_template = Template("""MERGE (i)-[r:$SubResourceRelLabel]->(j)""")
|
|
197
233
|
|
|
198
|
-
rel_merge_clause = rel_merge_template.safe_substitute(
|
|
234
|
+
rel_merge_clause = rel_merge_template.safe_substitute(
|
|
235
|
+
SubResourceRelLabel=sub_resource_link.rel_label,
|
|
236
|
+
)
|
|
199
237
|
|
|
200
|
-
rel_props_as_dict: Dict[str, PropertyRef] = _asdict_with_validate_relprops(
|
|
238
|
+
rel_props_as_dict: Dict[str, PropertyRef] = _asdict_with_validate_relprops(
|
|
239
|
+
sub_resource_link,
|
|
240
|
+
)
|
|
201
241
|
|
|
202
242
|
attach_sub_resource_statement = sub_resource_attach_template.safe_substitute(
|
|
203
243
|
SubResourceLabel=sub_resource_link.target_node_label,
|
|
204
244
|
MatchClause=_build_match_clause(sub_resource_link.target_node_matcher),
|
|
205
245
|
RelMergeClause=rel_merge_clause,
|
|
206
246
|
SubResourceRelLabel=sub_resource_link.rel_label,
|
|
207
|
-
set_rel_properties_statement=_build_rel_properties_statement(
|
|
247
|
+
set_rel_properties_statement=_build_rel_properties_statement(
|
|
248
|
+
"r",
|
|
249
|
+
rel_props_as_dict,
|
|
250
|
+
),
|
|
208
251
|
)
|
|
209
252
|
return attach_sub_resource_statement
|
|
210
253
|
|
|
211
254
|
|
|
212
255
|
def _build_attach_additional_links_statement(
|
|
213
|
-
|
|
256
|
+
additional_relationships: Optional[OtherRelationships] = None,
|
|
214
257
|
) -> str:
|
|
215
258
|
"""
|
|
216
259
|
Generates a Neo4j statement to attach one or more CartographyRelSchemas to node(s) previously mentioned in the
|
|
@@ -222,7 +265,7 @@ def _build_attach_additional_links_statement(
|
|
|
222
265
|
labels, attribute keys, and directionality. If additional_relationships is None, return an empty string.
|
|
223
266
|
"""
|
|
224
267
|
if not additional_relationships:
|
|
225
|
-
return
|
|
268
|
+
return ""
|
|
226
269
|
|
|
227
270
|
additional_links_template = Template(
|
|
228
271
|
"""
|
|
@@ -243,9 +286,13 @@ def _build_attach_additional_links_statement(
|
|
|
243
286
|
rel_var = f"r{num}"
|
|
244
287
|
|
|
245
288
|
if link.direction == LinkDirection.INWARD:
|
|
246
|
-
rel_merge_template = Template(
|
|
289
|
+
rel_merge_template = Template(
|
|
290
|
+
"""MERGE (i)<-[$rel_var:$AddlRelLabel]-($node_var)""",
|
|
291
|
+
)
|
|
247
292
|
else:
|
|
248
|
-
rel_merge_template = Template(
|
|
293
|
+
rel_merge_template = Template(
|
|
294
|
+
"""MERGE (i)-[$rel_var:$AddlRelLabel]->($node_var)""",
|
|
295
|
+
)
|
|
249
296
|
|
|
250
297
|
rel_merge = rel_merge_template.safe_substitute(
|
|
251
298
|
rel_var=rel_var,
|
|
@@ -257,20 +304,26 @@ def _build_attach_additional_links_statement(
|
|
|
257
304
|
|
|
258
305
|
additional_ref = additional_links_template.safe_substitute(
|
|
259
306
|
AddlLabel=link.target_node_label,
|
|
260
|
-
WhereClause=_build_where_clause_for_rel_match(
|
|
307
|
+
WhereClause=_build_where_clause_for_rel_match(
|
|
308
|
+
node_var,
|
|
309
|
+
link.target_node_matcher,
|
|
310
|
+
),
|
|
261
311
|
node_var=node_var,
|
|
262
312
|
rel_var=rel_var,
|
|
263
313
|
RelMerge=rel_merge,
|
|
264
|
-
set_rel_properties_statement=_build_rel_properties_statement(
|
|
314
|
+
set_rel_properties_statement=_build_rel_properties_statement(
|
|
315
|
+
rel_var,
|
|
316
|
+
rel_props_as_dict,
|
|
317
|
+
),
|
|
265
318
|
)
|
|
266
319
|
links.append(additional_ref)
|
|
267
320
|
|
|
268
|
-
return
|
|
321
|
+
return "UNION".join(links)
|
|
269
322
|
|
|
270
323
|
|
|
271
324
|
def _build_attach_relationships_statement(
|
|
272
|
-
|
|
273
|
-
|
|
325
|
+
sub_resource_relationship: Optional[CartographyRelSchema],
|
|
326
|
+
other_relationships: Optional[OtherRelationships],
|
|
274
327
|
) -> str:
|
|
275
328
|
"""
|
|
276
329
|
Use Neo4j subqueries to attach sub resource and/or other relationships.
|
|
@@ -283,14 +336,22 @@ def _build_attach_relationships_statement(
|
|
|
283
336
|
if not sub_resource_relationship and not other_relationships:
|
|
284
337
|
return ""
|
|
285
338
|
|
|
286
|
-
attach_sub_resource_statement = _build_attach_sub_resource_statement(
|
|
287
|
-
|
|
339
|
+
attach_sub_resource_statement = _build_attach_sub_resource_statement(
|
|
340
|
+
sub_resource_relationship,
|
|
341
|
+
)
|
|
342
|
+
attach_additional_links_statement = _build_attach_additional_links_statement(
|
|
343
|
+
other_relationships,
|
|
344
|
+
)
|
|
288
345
|
|
|
289
346
|
statements = []
|
|
290
|
-
statements +=
|
|
291
|
-
|
|
347
|
+
statements += (
|
|
348
|
+
[attach_sub_resource_statement] if attach_sub_resource_statement else []
|
|
349
|
+
)
|
|
350
|
+
statements += (
|
|
351
|
+
[attach_additional_links_statement] if attach_additional_links_statement else []
|
|
352
|
+
)
|
|
292
353
|
|
|
293
|
-
attach_relationships_statement =
|
|
354
|
+
attach_relationships_statement = "UNION".join(stmt for stmt in statements)
|
|
294
355
|
|
|
295
356
|
query_template = Template(
|
|
296
357
|
"""
|
|
@@ -300,12 +361,14 @@ def _build_attach_relationships_statement(
|
|
|
300
361
|
}
|
|
301
362
|
""",
|
|
302
363
|
)
|
|
303
|
-
return query_template.safe_substitute(
|
|
364
|
+
return query_template.safe_substitute(
|
|
365
|
+
attach_relationships_statement=attach_relationships_statement,
|
|
366
|
+
)
|
|
304
367
|
|
|
305
368
|
|
|
306
369
|
def rel_present_on_node_schema(
|
|
307
|
-
|
|
308
|
-
|
|
370
|
+
node_schema: CartographyNodeSchema,
|
|
371
|
+
rel_schema: CartographyRelSchema,
|
|
309
372
|
) -> bool:
|
|
310
373
|
"""
|
|
311
374
|
Answers the question: is the given rel_schema is present on the given node_schema?
|
|
@@ -317,8 +380,8 @@ def rel_present_on_node_schema(
|
|
|
317
380
|
|
|
318
381
|
|
|
319
382
|
def filter_selected_relationships(
|
|
320
|
-
|
|
321
|
-
|
|
383
|
+
node_schema: CartographyNodeSchema,
|
|
384
|
+
selected_relationships: Set[CartographyRelSchema],
|
|
322
385
|
) -> Tuple[Optional[CartographyRelSchema], Optional[OtherRelationships]]:
|
|
323
386
|
"""
|
|
324
387
|
Ensures that selected relationships specified to build_ingestion_query() are actually present on
|
|
@@ -353,14 +416,16 @@ def filter_selected_relationships(
|
|
|
353
416
|
sub_resource_rel = None
|
|
354
417
|
|
|
355
418
|
# By this point, everything in selected_relationships is validated to be present in node_schema
|
|
356
|
-
filtered_other_rels = OtherRelationships(
|
|
419
|
+
filtered_other_rels = OtherRelationships(
|
|
420
|
+
[rel for rel in selected_relationships if rel != sub_resource_rel],
|
|
421
|
+
)
|
|
357
422
|
|
|
358
423
|
return sub_resource_rel, filtered_other_rels
|
|
359
424
|
|
|
360
425
|
|
|
361
426
|
def build_ingestion_query(
|
|
362
|
-
|
|
363
|
-
|
|
427
|
+
node_schema: CartographyNodeSchema,
|
|
428
|
+
selected_relationships: Optional[Set[CartographyRelSchema]] = None,
|
|
364
429
|
) -> str:
|
|
365
430
|
"""
|
|
366
431
|
Generates a Neo4j query from the given CartographyNodeSchema to ingest the specified nodes and relationships so that
|
|
@@ -396,10 +461,15 @@ def build_ingestion_query(
|
|
|
396
461
|
node_props_as_dict: Dict[str, PropertyRef] = asdict(node_props)
|
|
397
462
|
|
|
398
463
|
# Handle selected relationships
|
|
399
|
-
sub_resource_rel: Optional[CartographyRelSchema] =
|
|
464
|
+
sub_resource_rel: Optional[CartographyRelSchema] = (
|
|
465
|
+
node_schema.sub_resource_relationship
|
|
466
|
+
)
|
|
400
467
|
other_rels: Optional[OtherRelationships] = node_schema.other_relationships
|
|
401
468
|
if selected_relationships or selected_relationships == set():
|
|
402
|
-
sub_resource_rel, other_rels = filter_selected_relationships(
|
|
469
|
+
sub_resource_rel, other_rels = filter_selected_relationships(
|
|
470
|
+
node_schema,
|
|
471
|
+
selected_relationships,
|
|
472
|
+
)
|
|
403
473
|
|
|
404
474
|
ingest_query = query_template.safe_substitute(
|
|
405
475
|
node_label=node_schema.label,
|
|
@@ -408,7 +478,10 @@ def build_ingestion_query(
|
|
|
408
478
|
node_props_as_dict,
|
|
409
479
|
node_schema.extra_node_labels,
|
|
410
480
|
),
|
|
411
|
-
attach_relationships_statement=_build_attach_relationships_statement(
|
|
481
|
+
attach_relationships_statement=_build_attach_relationships_statement(
|
|
482
|
+
sub_resource_rel,
|
|
483
|
+
other_rels,
|
|
484
|
+
),
|
|
412
485
|
)
|
|
413
486
|
return ingest_query
|
|
414
487
|
|
|
@@ -420,26 +493,31 @@ def build_create_index_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
|
420
493
|
:param node_schema: The Cartography node_schema object
|
|
421
494
|
:return: A list of queries of the form `CREATE INDEX IF NOT EXISTS FOR (n:$TargetNodeLabel) ON (n.$TargetAttribute)`
|
|
422
495
|
"""
|
|
423
|
-
index_template = Template(
|
|
496
|
+
index_template = Template(
|
|
497
|
+
"CREATE INDEX IF NOT EXISTS FOR (n:$TargetNodeLabel) ON (n.$TargetAttribute);",
|
|
498
|
+
)
|
|
424
499
|
|
|
425
500
|
# First ensure an index exists for the node_schema and all extra labels on the `id` and `lastupdated` fields
|
|
426
501
|
result = [
|
|
427
502
|
index_template.safe_substitute(
|
|
428
503
|
TargetNodeLabel=node_schema.label,
|
|
429
|
-
TargetAttribute=
|
|
504
|
+
TargetAttribute="id",
|
|
430
505
|
),
|
|
431
506
|
index_template.safe_substitute(
|
|
432
507
|
TargetNodeLabel=node_schema.label,
|
|
433
|
-
TargetAttribute=
|
|
508
|
+
TargetAttribute="lastupdated",
|
|
434
509
|
),
|
|
435
510
|
]
|
|
436
511
|
if node_schema.extra_node_labels:
|
|
437
|
-
result.extend(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
512
|
+
result.extend(
|
|
513
|
+
[
|
|
514
|
+
index_template.safe_substitute(
|
|
515
|
+
TargetNodeLabel=label,
|
|
516
|
+
TargetAttribute="id", # Precondition: 'id' is defined on all cartography node_schema objects.
|
|
517
|
+
)
|
|
518
|
+
for label in node_schema.extra_node_labels.labels
|
|
519
|
+
],
|
|
520
|
+
)
|
|
443
521
|
|
|
444
522
|
# Next, for all relationships possible out of this node, ensure that indexes exist for all target nodes' properties
|
|
445
523
|
# as specified in their TargetNodeMatchers.
|
|
@@ -451,15 +529,22 @@ def build_create_index_queries(node_schema: CartographyNodeSchema) -> List[str]:
|
|
|
451
529
|
for rs in rel_schemas:
|
|
452
530
|
for target_key in asdict(rs.target_node_matcher).keys():
|
|
453
531
|
result.append(
|
|
454
|
-
index_template.safe_substitute(
|
|
532
|
+
index_template.safe_substitute(
|
|
533
|
+
TargetNodeLabel=rs.target_node_label,
|
|
534
|
+
TargetAttribute=target_key,
|
|
535
|
+
),
|
|
455
536
|
)
|
|
456
537
|
|
|
457
538
|
# Now, include extra indexes defined by the module author on the node schema's property refs.
|
|
458
539
|
node_props_as_dict: Dict[str, PropertyRef] = asdict(node_schema.properties)
|
|
459
|
-
result.extend(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
540
|
+
result.extend(
|
|
541
|
+
[
|
|
542
|
+
index_template.safe_substitute(
|
|
543
|
+
TargetNodeLabel=node_schema.label,
|
|
544
|
+
TargetAttribute=prop_name,
|
|
545
|
+
)
|
|
546
|
+
for prop_name, prop_ref in node_props_as_dict.items()
|
|
547
|
+
if prop_ref.extra_index
|
|
548
|
+
],
|
|
549
|
+
)
|
|
465
550
|
return result
|
cartography/graph/statement.py
CHANGED
|
@@ -11,7 +11,6 @@ import neo4j
|
|
|
11
11
|
|
|
12
12
|
from cartography.stats import get_stats_client
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
logger = logging.getLogger(__name__)
|
|
16
15
|
stat_handler = get_stats_client(__name__)
|
|
17
16
|
|
|
@@ -41,13 +40,13 @@ class GraphStatement:
|
|
|
41
40
|
"""
|
|
42
41
|
|
|
43
42
|
def __init__(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
self,
|
|
44
|
+
query: str,
|
|
45
|
+
parameters: Optional[Dict[Any, Any]] = None,
|
|
46
|
+
iterative: bool = False,
|
|
47
|
+
iterationsize: int = 0,
|
|
48
|
+
parent_job_name: Optional[str] = None,
|
|
49
|
+
parent_job_sequence_num: Optional[int] = None,
|
|
51
50
|
):
|
|
52
51
|
self.query = query
|
|
53
52
|
self.parameters = parameters or {}
|
|
@@ -56,7 +55,9 @@ class GraphStatement:
|
|
|
56
55
|
self.parameters["LIMIT_SIZE"] = self.iterationsize
|
|
57
56
|
|
|
58
57
|
self.parent_job_name = parent_job_name if parent_job_name else None
|
|
59
|
-
self.parent_job_sequence_num =
|
|
58
|
+
self.parent_job_sequence_num = (
|
|
59
|
+
parent_job_sequence_num if parent_job_sequence_num else None
|
|
60
|
+
)
|
|
60
61
|
|
|
61
62
|
def merge_parameters(self, parameters: Dict) -> None:
|
|
62
63
|
"""
|
|
@@ -75,7 +76,9 @@ class GraphStatement:
|
|
|
75
76
|
else:
|
|
76
77
|
session.write_transaction(self._run_noniterative)
|
|
77
78
|
|
|
78
|
-
logger.info(
|
|
79
|
+
logger.info(
|
|
80
|
+
f"Completed {self.parent_job_name} statement #{self.parent_job_sequence_num}"
|
|
81
|
+
)
|
|
79
82
|
|
|
80
83
|
def as_dict(self) -> Dict[str, Any]:
|
|
81
84
|
"""
|
|
@@ -99,17 +102,21 @@ class GraphStatement:
|
|
|
99
102
|
summary: neo4j.ResultSummary = result.consume()
|
|
100
103
|
|
|
101
104
|
# Handle stats
|
|
102
|
-
stat_handler.incr(
|
|
103
|
-
stat_handler.incr(
|
|
104
|
-
stat_handler.incr(
|
|
105
|
-
stat_handler.incr(
|
|
106
|
-
stat_handler.incr(
|
|
107
|
-
stat_handler.incr(
|
|
108
|
-
stat_handler.incr(
|
|
109
|
-
stat_handler.incr(
|
|
110
|
-
stat_handler.incr(
|
|
111
|
-
stat_handler.incr(
|
|
112
|
-
|
|
105
|
+
stat_handler.incr("constraints_added", summary.counters.constraints_added)
|
|
106
|
+
stat_handler.incr("constraints_removed", summary.counters.constraints_removed)
|
|
107
|
+
stat_handler.incr("indexes_added", summary.counters.indexes_added)
|
|
108
|
+
stat_handler.incr("indexes_removed", summary.counters.indexes_removed)
|
|
109
|
+
stat_handler.incr("labels_added", summary.counters.labels_added)
|
|
110
|
+
stat_handler.incr("labels_removed", summary.counters.labels_removed)
|
|
111
|
+
stat_handler.incr("nodes_created", summary.counters.nodes_created)
|
|
112
|
+
stat_handler.incr("nodes_deleted", summary.counters.nodes_deleted)
|
|
113
|
+
stat_handler.incr("properties_set", summary.counters.properties_set)
|
|
114
|
+
stat_handler.incr(
|
|
115
|
+
"relationships_created", summary.counters.relationships_created
|
|
116
|
+
)
|
|
117
|
+
stat_handler.incr(
|
|
118
|
+
"relationships_deleted", summary.counters.relationships_deleted
|
|
119
|
+
)
|
|
113
120
|
|
|
114
121
|
return summary
|
|
115
122
|
|
|
@@ -122,17 +129,19 @@ class GraphStatement:
|
|
|
122
129
|
self.parameters["LIMIT_SIZE"] = self.iterationsize
|
|
123
130
|
|
|
124
131
|
while True:
|
|
125
|
-
summary: neo4j.ResultSummary = session.write_transaction(
|
|
132
|
+
summary: neo4j.ResultSummary = session.write_transaction(
|
|
133
|
+
self._run_noniterative
|
|
134
|
+
)
|
|
126
135
|
|
|
127
136
|
if not summary.counters.contains_updates:
|
|
128
137
|
break
|
|
129
138
|
|
|
130
139
|
@classmethod
|
|
131
140
|
def create_from_json(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
cls,
|
|
142
|
+
json_obj: Dict[str, Any],
|
|
143
|
+
short_job_name: Optional[str] = None,
|
|
144
|
+
job_sequence_num: Optional[int] = None,
|
|
136
145
|
):
|
|
137
146
|
"""
|
|
138
147
|
Create a statement from a JSON blob.
|
cartography/intel/analysis.py
CHANGED
|
@@ -39,4 +39,7 @@ def run(neo4j_session: neo4j.Session, config: Config) -> None:
|
|
|
39
39
|
except (KeyboardInterrupt, SystemExit):
|
|
40
40
|
raise
|
|
41
41
|
except Exception:
|
|
42
|
-
logger.exception(
|
|
42
|
+
logger.exception(
|
|
43
|
+
"An exception occurred while executing discovered analysis job: %s",
|
|
44
|
+
path,
|
|
45
|
+
)
|