cartography 0.107.0rc2__py3-none-any.whl → 0.108.0__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 (58) hide show
  1. cartography/_version.py +2 -2
  2. cartography/cli.py +10 -0
  3. cartography/config.py +5 -0
  4. cartography/data/indexes.cypher +0 -10
  5. cartography/data/jobs/cleanup/github_repos_cleanup.json +2 -0
  6. cartography/intel/aws/__init__.py +1 -0
  7. cartography/intel/aws/cloudtrail.py +17 -4
  8. cartography/intel/aws/cloudtrail_management_events.py +560 -16
  9. cartography/intel/aws/cloudwatch.py +150 -4
  10. cartography/intel/aws/ec2/security_groups.py +140 -122
  11. cartography/intel/aws/ec2/snapshots.py +47 -84
  12. cartography/intel/aws/ec2/subnets.py +37 -63
  13. cartography/intel/aws/ecr.py +55 -80
  14. cartography/intel/aws/ecs.py +17 -0
  15. cartography/intel/aws/elasticache.py +102 -79
  16. cartography/intel/aws/guardduty.py +275 -0
  17. cartography/intel/aws/resources.py +2 -0
  18. cartography/intel/aws/secretsmanager.py +62 -44
  19. cartography/intel/github/repos.py +370 -28
  20. cartography/intel/sentinelone/__init__.py +8 -2
  21. cartography/intel/sentinelone/application.py +248 -0
  22. cartography/intel/sentinelone/utils.py +20 -1
  23. cartography/models/aws/cloudtrail/management_events.py +95 -6
  24. cartography/models/aws/cloudtrail/trail.py +21 -0
  25. cartography/models/aws/cloudwatch/log_metric_filter.py +79 -0
  26. cartography/models/aws/cloudwatch/metric_alarm.py +53 -0
  27. cartography/models/aws/ec2/networkinterfaces.py +2 -0
  28. cartography/models/aws/ec2/security_group_rules.py +109 -0
  29. cartography/models/aws/ec2/security_groups.py +90 -0
  30. cartography/models/aws/ec2/snapshots.py +58 -0
  31. cartography/models/aws/ec2/subnet_instance.py +2 -0
  32. cartography/models/aws/ec2/subnet_networkinterface.py +2 -0
  33. cartography/models/aws/ec2/subnets.py +65 -0
  34. cartography/models/aws/ec2/volumes.py +20 -0
  35. cartography/models/aws/ecr/__init__.py +0 -0
  36. cartography/models/aws/ecr/image.py +41 -0
  37. cartography/models/aws/ecr/repository.py +72 -0
  38. cartography/models/aws/ecr/repository_image.py +95 -0
  39. cartography/models/aws/ecs/tasks.py +24 -1
  40. cartography/models/aws/elasticache/__init__.py +0 -0
  41. cartography/models/aws/elasticache/cluster.py +65 -0
  42. cartography/models/aws/elasticache/topic.py +67 -0
  43. cartography/models/aws/guardduty/__init__.py +1 -0
  44. cartography/models/aws/guardduty/findings.py +102 -0
  45. cartography/models/aws/secretsmanager/secret.py +106 -0
  46. cartography/models/github/dependencies.py +74 -0
  47. cartography/models/github/manifests.py +49 -0
  48. cartography/models/sentinelone/application.py +44 -0
  49. cartography/models/sentinelone/application_version.py +96 -0
  50. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/METADATA +3 -3
  51. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/RECORD +55 -36
  52. cartography/data/jobs/cleanup/aws_import_ec2_security_groupinfo_cleanup.json +0 -24
  53. cartography/data/jobs/cleanup/aws_import_secrets_cleanup.json +0 -8
  54. cartography/data/jobs/cleanup/aws_import_snapshots_cleanup.json +0 -30
  55. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/WHEEL +0 -0
  56. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/entry_points.txt +0 -0
  57. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/licenses/LICENSE +0 -0
  58. {cartography-0.107.0rc2.dist-info → cartography-0.108.0.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,11 @@ import neo4j
9
9
  from cartography.client.core.tx import load
10
10
  from cartography.graph.job import GraphJob
11
11
  from cartography.intel.aws.ec2.util import get_botocore_config
12
+ from cartography.models.aws.cloudwatch.log_metric_filter import (
13
+ CloudWatchLogMetricFilterSchema,
14
+ )
12
15
  from cartography.models.aws.cloudwatch.loggroup import CloudWatchLogGroupSchema
16
+ from cartography.models.aws.cloudwatch.metric_alarm import CloudWatchMetricAlarmSchema
13
17
  from cartography.util import aws_handle_regions
14
18
  from cartography.util import timeit
15
19
 
@@ -31,6 +35,84 @@ def get_cloudwatch_log_groups(
31
35
  return logGroups
32
36
 
33
37
 
38
+ @timeit
39
+ @aws_handle_regions
40
+ def get_cloudwatch_log_metric_filters(
41
+ boto3_session: boto3.Session, region: str
42
+ ) -> List[Dict[str, Any]]:
43
+ logs_client = boto3_session.client(
44
+ "logs", region_name=region, config=get_botocore_config()
45
+ )
46
+ paginator = logs_client.get_paginator("describe_metric_filters")
47
+ metric_filters = []
48
+
49
+ for page in paginator.paginate():
50
+ metric_filters.extend(page.get("metricFilters", []))
51
+
52
+ return metric_filters
53
+
54
+
55
+ def transform_metric_filters(
56
+ metric_filters: List[Dict[str, Any]], region: str
57
+ ) -> List[Dict[str, Any]]:
58
+ """
59
+ Transform CloudWatch log metric filter data for ingestion into Neo4j.
60
+ Ensures that the 'id' field is a unique combination of logGroupName and filterName.
61
+ """
62
+ transformed_filters = []
63
+ for filter in metric_filters:
64
+ transformed_filter = {
65
+ "id": f"{filter['logGroupName']}:{filter['filterName']}",
66
+ "arn": f"{filter['logGroupName']}:{filter['filterName']}",
67
+ "filterName": filter["filterName"],
68
+ "filterPattern": filter.get("filterPattern"),
69
+ "logGroupName": filter["logGroupName"],
70
+ "metricName": filter["metricTransformations"][0]["metricName"],
71
+ "metricNamespace": filter["metricTransformations"][0]["metricNamespace"],
72
+ "metricValue": filter["metricTransformations"][0]["metricValue"],
73
+ "Region": region,
74
+ }
75
+ transformed_filters.append(transformed_filter)
76
+ return transformed_filters
77
+
78
+
79
+ @timeit
80
+ @aws_handle_regions
81
+ def get_cloudwatch_metric_alarms(
82
+ boto3_session: boto3.Session, region: str
83
+ ) -> List[Dict[str, Any]]:
84
+ client = boto3_session.client(
85
+ "cloudwatch", region_name=region, config=get_botocore_config()
86
+ )
87
+ paginator = client.get_paginator("describe_alarms")
88
+ alarms = []
89
+ for page in paginator.paginate():
90
+ alarms.extend(page["MetricAlarms"])
91
+ return alarms
92
+
93
+
94
+ def transform_metric_alarms(
95
+ metric_alarms: List[Dict[str, Any]], region: str
96
+ ) -> List[Dict[str, Any]]:
97
+ """
98
+ Transform CloudWatch metric alarm data for ingestion into Neo4j.
99
+ """
100
+ transformed_alarms = []
101
+ for alarm in metric_alarms:
102
+ transformed_alarm = {
103
+ "AlarmArn": alarm["AlarmArn"],
104
+ "AlarmName": alarm.get("AlarmName"),
105
+ "AlarmDescription": alarm.get("AlarmDescription"),
106
+ "StateValue": alarm.get("StateValue"),
107
+ "StateReason": alarm.get("StateReason"),
108
+ "ActionsEnabled": alarm.get("ActionsEnabled"),
109
+ "ComparisonOperator": alarm.get("ComparisonOperator"),
110
+ "Region": region,
111
+ }
112
+ transformed_alarms.append(transformed_alarm)
113
+ return transformed_alarms
114
+
115
+
34
116
  @timeit
35
117
  def load_cloudwatch_log_groups(
36
118
  neo4j_session: neo4j.Session,
@@ -52,6 +134,48 @@ def load_cloudwatch_log_groups(
52
134
  )
53
135
 
54
136
 
137
+ @timeit
138
+ def load_cloudwatch_log_metric_filters(
139
+ neo4j_session: neo4j.Session,
140
+ data: List[Dict[str, Any]],
141
+ region: str,
142
+ current_aws_account_id: str,
143
+ aws_update_tag: int,
144
+ ) -> None:
145
+ logger.info(
146
+ f"Loading CloudWatch {len(data)} log metric filters for region '{region}' into graph.",
147
+ )
148
+ load(
149
+ neo4j_session,
150
+ CloudWatchLogMetricFilterSchema(),
151
+ data,
152
+ lastupdated=aws_update_tag,
153
+ Region=region,
154
+ AWS_ID=current_aws_account_id,
155
+ )
156
+
157
+
158
+ @timeit
159
+ def load_cloudwatch_metric_alarms(
160
+ neo4j_session: neo4j.Session,
161
+ data: List[Dict[str, Any]],
162
+ region: str,
163
+ current_aws_account_id: str,
164
+ aws_update_tag: int,
165
+ ) -> None:
166
+ logger.info(
167
+ f"Loading CloudWatch {len(data)} metric alarms for region '{region}' into graph.",
168
+ )
169
+ load(
170
+ neo4j_session,
171
+ CloudWatchMetricAlarmSchema(),
172
+ data,
173
+ lastupdated=aws_update_tag,
174
+ Region=region,
175
+ AWS_ID=current_aws_account_id,
176
+ )
177
+
178
+
55
179
  @timeit
56
180
  def cleanup(
57
181
  neo4j_session: neo4j.Session,
@@ -62,6 +186,12 @@ def cleanup(
62
186
  CloudWatchLogGroupSchema(), common_job_parameters
63
187
  )
64
188
  cleanup_job.run(neo4j_session)
189
+ GraphJob.from_node_schema(
190
+ CloudWatchLogMetricFilterSchema(), common_job_parameters
191
+ ).run(neo4j_session)
192
+ GraphJob.from_node_schema(CloudWatchMetricAlarmSchema(), common_job_parameters).run(
193
+ neo4j_session
194
+ )
65
195
 
66
196
 
67
197
  @timeit
@@ -78,16 +208,32 @@ def sync(
78
208
  f"Syncing CloudWatch for region '{region}' in account '{current_aws_account_id}'.",
79
209
  )
80
210
  logGroups = get_cloudwatch_log_groups(boto3_session, region)
81
- group_data: List[Dict[str, Any]] = []
82
- for logGroup in logGroups:
83
- group_data.append(logGroup)
84
211
 
85
212
  load_cloudwatch_log_groups(
86
213
  neo4j_session,
87
- group_data,
214
+ logGroups,
215
+ region,
216
+ current_aws_account_id,
217
+ update_tag,
218
+ )
219
+
220
+ metric_filters = get_cloudwatch_log_metric_filters(boto3_session, region)
221
+ transformed_filters = transform_metric_filters(metric_filters, region)
222
+ load_cloudwatch_log_metric_filters(
223
+ neo4j_session,
224
+ transformed_filters,
88
225
  region,
89
226
  current_aws_account_id,
90
227
  update_tag,
91
228
  )
92
229
 
230
+ metric_alarms = get_cloudwatch_metric_alarms(boto3_session, region)
231
+ transformed_alarms = transform_metric_alarms(metric_alarms, region)
232
+ load_cloudwatch_metric_alarms(
233
+ neo4j_session,
234
+ transformed_alarms,
235
+ region,
236
+ current_aws_account_id,
237
+ update_tag,
238
+ )
93
239
  cleanup(neo4j_session, common_job_parameters)
@@ -1,17 +1,22 @@
1
1
  import logging
2
- from string import Template
2
+ from collections import namedtuple
3
+ from typing import Any
3
4
  from typing import Dict
4
5
  from typing import List
5
6
 
6
7
  import boto3
7
8
  import neo4j
8
9
 
10
+ from cartography.client.core.tx import load
9
11
  from cartography.graph.job import GraphJob
12
+ from cartography.models.aws.ec2.security_group_rules import IpPermissionInboundSchema
13
+ from cartography.models.aws.ec2.security_group_rules import IpRangeSchema
14
+ from cartography.models.aws.ec2.security_group_rules import IpRuleSchema
15
+ from cartography.models.aws.ec2.security_groups import EC2SecurityGroupSchema
10
16
  from cartography.models.aws.ec2.securitygroup_instance import (
11
17
  EC2SecurityGroupInstanceSchema,
12
18
  )
13
19
  from cartography.util import aws_handle_regions
14
- from cartography.util import run_cleanup_job
15
20
  from cartography.util import timeit
16
21
 
17
22
  from .util import get_botocore_config
@@ -37,138 +42,146 @@ def get_ec2_security_group_data(
37
42
  return security_groups
38
43
 
39
44
 
45
+ Ec2SecurityGroupData = namedtuple(
46
+ "Ec2SecurityGroupData",
47
+ ["groups", "inbound_rules", "egress_rules", "ranges"],
48
+ )
49
+
50
+
51
+ def transform_ec2_security_group_data(
52
+ data: List[Dict[str, Any]],
53
+ ) -> Ec2SecurityGroupData:
54
+ groups: List[Dict[str, Any]] = []
55
+ inbound_rules: List[Dict[str, Any]] = []
56
+ egress_rules: List[Dict[str, Any]] = []
57
+ ranges: List[Dict[str, Any]] = []
58
+
59
+ for group in data:
60
+ group_record = {
61
+ "GroupId": group["GroupId"],
62
+ "GroupName": group.get("GroupName"),
63
+ "Description": group.get("Description"),
64
+ "VpcId": group.get("VpcId"),
65
+ }
66
+ # Collect referenced security groups for relationship loading
67
+ source_group_ids: set[str] = set()
68
+
69
+ for rule_type, target in (
70
+ ("IpPermissions", inbound_rules),
71
+ ("IpPermissionsEgress", egress_rules),
72
+ ):
73
+ for rule in group.get(rule_type, []):
74
+ protocol = rule.get("IpProtocol", "all")
75
+ from_port = rule.get("FromPort")
76
+ to_port = rule.get("ToPort")
77
+ rule_id = (
78
+ f"{group['GroupId']}/{rule_type}/{from_port}{to_port}{protocol}"
79
+ )
80
+ target.append(
81
+ {
82
+ "RuleId": rule_id,
83
+ "GroupId": group["GroupId"],
84
+ "Protocol": protocol,
85
+ "FromPort": from_port,
86
+ "ToPort": to_port,
87
+ },
88
+ )
89
+ for ip_range in rule.get("IpRanges", []):
90
+ ranges.append({"RangeId": ip_range["CidrIp"], "RuleId": rule_id})
91
+ for pair in rule.get("UserIdGroupPairs", []):
92
+ sg_id = pair.get("GroupId")
93
+ if sg_id:
94
+ source_group_ids.add(sg_id)
95
+
96
+ group_record["SOURCE_GROUP_IDS"] = list(source_group_ids)
97
+ groups.append(group_record)
98
+
99
+ return Ec2SecurityGroupData(
100
+ groups=groups,
101
+ inbound_rules=inbound_rules,
102
+ egress_rules=egress_rules,
103
+ ranges=ranges,
104
+ )
105
+
106
+
40
107
  @timeit
41
- def load_ec2_security_group_rule(
108
+ def load_ip_rules(
42
109
  neo4j_session: neo4j.Session,
43
- group: Dict,
44
- rule_type: str,
110
+ data: List[Dict[str, Any]],
111
+ inbound: bool,
112
+ region: str,
113
+ aws_account_id: str,
45
114
  update_tag: int,
46
115
  ) -> None:
47
- INGEST_RULE_TEMPLATE = Template(
48
- """
49
- MERGE (rule:$rule_label{ruleid: $RuleId})
50
- ON CREATE SET rule :IpRule, rule.firstseen = timestamp(), rule.fromport = $FromPort, rule.toport = $ToPort,
51
- rule.protocol = $Protocol
52
- SET rule.lastupdated = $update_tag
53
- WITH rule
54
- MATCH (group:EC2SecurityGroup{groupid: $GroupId})
55
- MERGE (group)<-[r:MEMBER_OF_EC2_SECURITY_GROUP]-(rule)
56
- ON CREATE SET r.firstseen = timestamp()
57
- SET r.lastupdated = $update_tag;
58
- """,
116
+ schema = IpPermissionInboundSchema() if inbound else IpRuleSchema()
117
+ load(
118
+ neo4j_session,
119
+ schema,
120
+ data,
121
+ Region=region,
122
+ AWS_ID=aws_account_id,
123
+ lastupdated=update_tag,
59
124
  )
60
125
 
61
- ingest_rule_group_pair = """
62
- MERGE (group:EC2SecurityGroup{id: $GroupId})
63
- ON CREATE SET group.firstseen = timestamp(), group.groupid = $GroupId
64
- SET group.lastupdated = $update_tag
65
- WITH group
66
- MATCH (inbound:IpRule{ruleid: $RuleId})
67
- MERGE (inbound)-[r:MEMBER_OF_EC2_SECURITY_GROUP]->(group)
68
- ON CREATE SET r.firstseen = timestamp()
69
- SET r.lastupdated = $update_tag
70
- """
71
-
72
- ingest_range = """
73
- MERGE (range:IpRange{id: $RangeId})
74
- ON CREATE SET range.firstseen = timestamp(), range.range = $RangeId
75
- SET range.lastupdated = $update_tag
76
- WITH range
77
- MATCH (rule:IpRule{ruleid: $RuleId})
78
- MERGE (rule)<-[r:MEMBER_OF_IP_RULE]-(range)
79
- ON CREATE SET r.firstseen = timestamp()
80
- SET r.lastupdated = $update_tag
81
- """
82
-
83
- group_id = group["GroupId"]
84
- rule_type_map = {
85
- "IpPermissions": "IpPermissionInbound",
86
- "IpPermissionsEgress": "IpPermissionEgress",
87
- }
88
-
89
- if group.get(rule_type):
90
- for rule in group[rule_type]:
91
- protocol = rule.get("IpProtocol", "all")
92
- from_port = rule.get("FromPort")
93
- to_port = rule.get("ToPort")
94
-
95
- ruleid = f"{group_id}/{rule_type}/{from_port}{to_port}{protocol}"
96
- # NOTE Cypher query syntax is incompatible with Python string formatting, so we have to do this awkward
97
- # NOTE manual formatting instead.
98
- neo4j_session.run(
99
- INGEST_RULE_TEMPLATE.safe_substitute(
100
- rule_label=rule_type_map[rule_type],
101
- ),
102
- RuleId=ruleid,
103
- FromPort=from_port,
104
- ToPort=to_port,
105
- Protocol=protocol,
106
- GroupId=group_id,
107
- update_tag=update_tag,
108
- )
109
-
110
- neo4j_session.run(
111
- ingest_rule_group_pair,
112
- GroupId=group_id,
113
- RuleId=ruleid,
114
- update_tag=update_tag,
115
- )
116
-
117
- for ip_range in rule["IpRanges"]:
118
- range_id = ip_range["CidrIp"]
119
- neo4j_session.run(
120
- ingest_range,
121
- RangeId=range_id,
122
- RuleId=ruleid,
123
- update_tag=update_tag,
124
- )
126
+
127
+ @timeit
128
+ def load_ip_ranges(
129
+ neo4j_session: neo4j.Session,
130
+ data: List[Dict[str, Any]],
131
+ region: str,
132
+ aws_account_id: str,
133
+ update_tag: int,
134
+ ) -> None:
135
+ load(
136
+ neo4j_session,
137
+ IpRangeSchema(),
138
+ data,
139
+ Region=region,
140
+ AWS_ID=aws_account_id,
141
+ lastupdated=update_tag,
142
+ )
125
143
 
126
144
 
127
145
  @timeit
128
146
  def load_ec2_security_groupinfo(
129
147
  neo4j_session: neo4j.Session,
130
- data: List[Dict],
148
+ data: Ec2SecurityGroupData,
131
149
  region: str,
132
150
  current_aws_account_id: str,
133
151
  update_tag: int,
134
152
  ) -> None:
135
- ingest_security_group = """
136
- MERGE (group:EC2SecurityGroup{id: $GroupId})
137
- ON CREATE SET group.firstseen = timestamp(), group.groupid = $GroupId
138
- SET group.name = $GroupName, group.description = $Description, group.region = $Region,
139
- group.lastupdated = $update_tag
140
- WITH group
141
- MATCH (aa:AWSAccount{id: $AWS_ACCOUNT_ID})
142
- MERGE (aa)-[r:RESOURCE]->(group)
143
- ON CREATE SET r.firstseen = timestamp()
144
- SET r.lastupdated = $update_tag
145
- WITH group
146
- MATCH (vpc:AWSVpc{id: $VpcId})
147
- MERGE (vpc)-[rg:MEMBER_OF_EC2_SECURITY_GROUP]->(group)
148
- ON CREATE SET rg.firstseen = timestamp()
149
- """
150
-
151
- for group in data:
152
- group_id = group["GroupId"]
153
-
154
- neo4j_session.run(
155
- ingest_security_group,
156
- GroupId=group_id,
157
- GroupName=group.get("GroupName"),
158
- Description=group.get("Description"),
159
- VpcId=group.get("VpcId", None),
160
- Region=region,
161
- AWS_ACCOUNT_ID=current_aws_account_id,
162
- update_tag=update_tag,
163
- )
153
+ load(
154
+ neo4j_session,
155
+ EC2SecurityGroupSchema(),
156
+ data.groups,
157
+ Region=region,
158
+ AWS_ID=current_aws_account_id,
159
+ lastupdated=update_tag,
160
+ )
164
161
 
165
- load_ec2_security_group_rule(neo4j_session, group, "IpPermissions", update_tag)
166
- load_ec2_security_group_rule(
167
- neo4j_session,
168
- group,
169
- "IpPermissionsEgress",
170
- update_tag,
171
- )
162
+ load_ip_rules(
163
+ neo4j_session,
164
+ data.inbound_rules,
165
+ inbound=True,
166
+ region=region,
167
+ aws_account_id=current_aws_account_id,
168
+ update_tag=update_tag,
169
+ )
170
+ load_ip_rules(
171
+ neo4j_session,
172
+ data.egress_rules,
173
+ inbound=False,
174
+ region=region,
175
+ aws_account_id=current_aws_account_id,
176
+ update_tag=update_tag,
177
+ )
178
+ load_ip_ranges(
179
+ neo4j_session,
180
+ data.ranges,
181
+ region,
182
+ current_aws_account_id,
183
+ update_tag,
184
+ )
172
185
 
173
186
 
174
187
  @timeit
@@ -176,11 +189,15 @@ def cleanup_ec2_security_groupinfo(
176
189
  neo4j_session: neo4j.Session,
177
190
  common_job_parameters: Dict,
178
191
  ) -> None:
179
- run_cleanup_job(
180
- "aws_import_ec2_security_groupinfo_cleanup.json",
181
- neo4j_session,
192
+ GraphJob.from_node_schema(
193
+ EC2SecurityGroupSchema(),
182
194
  common_job_parameters,
195
+ ).run(neo4j_session)
196
+ GraphJob.from_node_schema(IpPermissionInboundSchema(), common_job_parameters).run(
197
+ neo4j_session,
183
198
  )
199
+ GraphJob.from_node_schema(IpRuleSchema(), common_job_parameters).run(neo4j_session)
200
+ GraphJob.from_node_schema(IpRangeSchema(), common_job_parameters).run(neo4j_session)
184
201
  GraphJob.from_node_schema(
185
202
  EC2SecurityGroupInstanceSchema(),
186
203
  common_job_parameters,
@@ -203,9 +220,10 @@ def sync_ec2_security_groupinfo(
203
220
  current_aws_account_id,
204
221
  )
205
222
  data = get_ec2_security_group_data(boto3_session, region)
223
+ transformed = transform_ec2_security_group_data(data)
206
224
  load_ec2_security_groupinfo(
207
225
  neo4j_session,
208
- data,
226
+ transformed,
209
227
  region,
210
228
  current_aws_account_id,
211
229
  update_tag,