cartography 0.114.0__py3-none-any.whl → 0.116.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 +2 -2
  3. cartography/client/core/tx.py +12 -1
  4. cartography/intel/aws/config.py +7 -3
  5. cartography/intel/aws/ecr.py +9 -9
  6. cartography/intel/aws/ecr_image_layers.py +664 -0
  7. cartography/intel/aws/identitycenter.py +240 -13
  8. cartography/intel/aws/lambda_function.py +69 -2
  9. cartography/intel/aws/organizations.py +3 -1
  10. cartography/intel/aws/permission_relationships.py +3 -1
  11. cartography/intel/aws/redshift.py +9 -4
  12. cartography/intel/aws/resources.py +2 -0
  13. cartography/intel/aws/route53.py +53 -3
  14. cartography/intel/aws/securityhub.py +3 -1
  15. cartography/intel/azure/__init__.py +16 -0
  16. cartography/intel/azure/logic_apps.py +101 -0
  17. cartography/intel/azure/resource_groups.py +82 -0
  18. cartography/intel/create_indexes.py +2 -1
  19. cartography/intel/dns.py +5 -2
  20. cartography/intel/gcp/dns.py +2 -1
  21. cartography/intel/github/repos.py +3 -6
  22. cartography/intel/gsuite/api.py +17 -4
  23. cartography/intel/okta/applications.py +9 -4
  24. cartography/intel/okta/awssaml.py +5 -2
  25. cartography/intel/okta/factors.py +3 -1
  26. cartography/intel/okta/groups.py +5 -2
  27. cartography/intel/okta/organization.py +3 -1
  28. cartography/intel/okta/origins.py +3 -1
  29. cartography/intel/okta/roles.py +5 -2
  30. cartography/intel/okta/users.py +3 -1
  31. cartography/models/aws/ecr/image.py +21 -0
  32. cartography/models/aws/ecr/image_layer.py +107 -0
  33. cartography/models/aws/identitycenter/awspermissionset.py +24 -1
  34. cartography/models/aws/identitycenter/awssogroup.py +70 -0
  35. cartography/models/aws/identitycenter/awsssouser.py +37 -1
  36. cartography/models/aws/lambda_function/lambda_function.py +2 -0
  37. cartography/models/azure/logic_apps.py +56 -0
  38. cartography/models/azure/resource_groups.py +52 -0
  39. cartography/models/entra/user.py +18 -0
  40. cartography/rules/README.md +1 -0
  41. cartography/rules/__init__.py +0 -0
  42. cartography/rules/cli.py +342 -0
  43. cartography/rules/data/__init__.py +0 -0
  44. cartography/rules/data/frameworks/__init__.py +12 -0
  45. cartography/rules/data/frameworks/mitre_attack/__init__.py +14 -0
  46. cartography/rules/data/frameworks/mitre_attack/requirements/__init__.py +0 -0
  47. cartography/rules/data/frameworks/mitre_attack/requirements/t1190_exploit_public_facing_application/__init__.py +135 -0
  48. cartography/rules/formatters.py +46 -0
  49. cartography/rules/runners.py +338 -0
  50. cartography/rules/spec/__init__.py +0 -0
  51. cartography/rules/spec/model.py +88 -0
  52. cartography/rules/spec/result.py +46 -0
  53. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/METADATA +19 -4
  54. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/RECORD +58 -38
  55. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/entry_points.txt +1 -0
  56. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/WHEEL +0 -0
  57. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/licenses/LICENSE +0 -0
  58. {cartography-0.114.0.dist-info → cartography-0.116.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,342 @@
1
+ """
2
+ Cartography RunRules CLI
3
+
4
+ Execute security frameworks and present facts about your environment.
5
+ """
6
+
7
+ import builtins
8
+ import logging
9
+ import os
10
+ from enum import Enum
11
+ from typing import Generator
12
+
13
+ import typer
14
+ from typing_extensions import Annotated
15
+
16
+ from cartography.rules.data.frameworks import FRAMEWORKS
17
+ from cartography.rules.runners import run_frameworks
18
+ from cartography.rules.spec.model import Fact
19
+ from cartography.rules.spec.model import Requirement
20
+
21
+ app = typer.Typer(
22
+ help="Execute Cartography security frameworks",
23
+ no_args_is_help=True,
24
+ )
25
+
26
+
27
+ class OutputFormat(str, Enum):
28
+ """Output format options."""
29
+
30
+ text = "text"
31
+ json = "json"
32
+
33
+
34
+ def complete_frameworks(incomplete: str) -> Generator[str, None, None]:
35
+ """Autocomplete framework names."""
36
+ for name in FRAMEWORKS.keys():
37
+ if name.startswith(incomplete):
38
+ yield name
39
+
40
+
41
+ def complete_frameworks_with_all(incomplete: str) -> Generator[str, None, None]:
42
+ """Autocomplete framework names plus 'all'."""
43
+ for name in builtins.list(FRAMEWORKS.keys()) + ["all"]:
44
+ if name.startswith(incomplete):
45
+ yield name
46
+
47
+
48
+ def complete_requirements(
49
+ ctx: typer.Context, incomplete: str
50
+ ) -> Generator[str, None, None]:
51
+ """Autocomplete requirement IDs based on selected framework."""
52
+ framework = ctx.params.get("framework")
53
+ if not framework or framework not in FRAMEWORKS:
54
+ return
55
+
56
+ for req in FRAMEWORKS[framework].requirements:
57
+ if req.id.lower().startswith(incomplete.lower()):
58
+ yield req.id
59
+
60
+
61
+ def complete_facts(ctx: typer.Context, incomplete: str) -> Generator[str, None, None]:
62
+ """Autocomplete fact IDs based on selected framework and requirement."""
63
+ framework = ctx.params.get("framework")
64
+ requirement_id = ctx.params.get("requirement")
65
+
66
+ if not framework or framework not in FRAMEWORKS:
67
+ return
68
+ if not requirement_id:
69
+ return
70
+
71
+ # Find the requirement
72
+ for req in FRAMEWORKS[framework].requirements:
73
+ if req.id.lower() == requirement_id.lower():
74
+ for fact in req.facts:
75
+ if fact.id.lower().startswith(incomplete.lower()):
76
+ yield fact.id
77
+ break
78
+
79
+
80
+ @app.command() # type: ignore[misc]
81
+ def list(
82
+ framework: Annotated[
83
+ str | None,
84
+ typer.Argument(
85
+ help="Framework name (e.g., mitre-attack)",
86
+ autocompletion=complete_frameworks,
87
+ ),
88
+ ] = None,
89
+ requirement: Annotated[
90
+ str | None,
91
+ typer.Argument(
92
+ help="Requirement ID (e.g., T1190)",
93
+ autocompletion=complete_requirements,
94
+ ),
95
+ ] = None,
96
+ ) -> None:
97
+ """
98
+ List available frameworks, requirements, and facts.
99
+
100
+ \b
101
+ Examples:
102
+ cartography-rules list
103
+ cartography-rules list mitre-attack
104
+ cartography-rules list mitre-attack T1190
105
+ """
106
+ # List all frameworks
107
+ if not framework:
108
+ typer.secho("\nAvailable Frameworks\n", bold=True)
109
+ for fw_name, fw in FRAMEWORKS.items():
110
+ typer.secho(f"{fw_name}", fg=typer.colors.CYAN)
111
+ typer.echo(f" Name: {fw.name}")
112
+ typer.echo(f" Version: {fw.version}")
113
+ typer.echo(f" Requirements: {len(fw.requirements)}")
114
+ total_facts = sum(len(req.facts) for req in fw.requirements)
115
+ typer.echo(f" Total Facts: {total_facts}")
116
+ if fw.source_url:
117
+ typer.echo(f" Source: {fw.source_url}")
118
+ typer.echo()
119
+ return
120
+
121
+ # Validate framework
122
+ if framework not in FRAMEWORKS:
123
+ typer.secho(
124
+ f"Error: Unknown framework '{framework}'", fg=typer.colors.RED, err=True
125
+ )
126
+ typer.echo(f"Available: {', '.join(FRAMEWORKS.keys())}", err=True)
127
+ raise typer.Exit(1)
128
+
129
+ fw = FRAMEWORKS[framework]
130
+
131
+ # List all requirements in framework
132
+ if not requirement:
133
+ typer.secho(f"\n{fw.name}", bold=True)
134
+ typer.echo(f"Version: {fw.version}\n")
135
+ for r in fw.requirements:
136
+ typer.secho(f"{r.id}", fg=typer.colors.CYAN)
137
+ typer.echo(f" Name: {r.name}")
138
+ typer.echo(f" Facts: {len(r.facts)}")
139
+ if r.requirement_url:
140
+ typer.echo(f" URL: {r.requirement_url}")
141
+ typer.echo()
142
+ return
143
+
144
+ # Find and list facts in requirement
145
+ req: Requirement | None = None
146
+ for r in fw.requirements:
147
+ if r.id.lower() == requirement.lower():
148
+ req = r
149
+ break
150
+
151
+ if not req:
152
+ typer.secho(
153
+ f"Error: Requirement '{requirement}' not found",
154
+ fg=typer.colors.RED,
155
+ err=True,
156
+ )
157
+ typer.echo("\nAvailable requirements:", err=True)
158
+ for r in fw.requirements:
159
+ typer.echo(f" {r.id}", err=True)
160
+ raise typer.Exit(1)
161
+
162
+ typer.secho(f"\n{req.name}\n", bold=True)
163
+ typer.echo(f"ID: {req.id}")
164
+ if req.requirement_url:
165
+ typer.echo(f"URL: {req.requirement_url}")
166
+ typer.secho(f"\nFacts ({len(req.facts)})\n", bold=True)
167
+
168
+ for fact in req.facts:
169
+ typer.secho(f"{fact.id}", fg=typer.colors.CYAN)
170
+ typer.echo(f" Name: {fact.name}")
171
+ typer.echo(f" Description: {fact.description}")
172
+ typer.echo(f" Provider: {fact.module.value}")
173
+ typer.echo()
174
+
175
+
176
+ @app.command() # type: ignore[misc]
177
+ def run(
178
+ framework: Annotated[
179
+ str,
180
+ typer.Argument(
181
+ help="Framework to execute (or 'all' for all frameworks)",
182
+ autocompletion=complete_frameworks_with_all,
183
+ ),
184
+ ],
185
+ requirement: Annotated[
186
+ str | None,
187
+ typer.Argument(
188
+ help="Specific requirement ID to run",
189
+ autocompletion=complete_requirements,
190
+ ),
191
+ ] = None,
192
+ fact: Annotated[
193
+ str | None,
194
+ typer.Argument(
195
+ help="Specific fact ID to run",
196
+ autocompletion=complete_facts,
197
+ ),
198
+ ] = None,
199
+ uri: Annotated[
200
+ str,
201
+ typer.Option(help="Neo4j URI", envvar="NEO4J_URI"),
202
+ ] = "bolt://localhost:7687",
203
+ user: Annotated[
204
+ str,
205
+ typer.Option(help="Neo4j username", envvar="NEO4J_USER"),
206
+ ] = "neo4j",
207
+ database: Annotated[
208
+ str,
209
+ typer.Option(help="Neo4j database name", envvar="NEO4J_DATABASE"),
210
+ ] = "neo4j",
211
+ neo4j_password_env_var: Annotated[
212
+ str | None,
213
+ typer.Option(help="Environment variable containing Neo4j password"),
214
+ ] = None,
215
+ neo4j_password_prompt: Annotated[
216
+ bool,
217
+ typer.Option(help="Prompt for Neo4j password interactively"),
218
+ ] = False,
219
+ output: Annotated[
220
+ OutputFormat,
221
+ typer.Option(help="Output format"),
222
+ ] = OutputFormat.text,
223
+ ) -> None:
224
+ """
225
+ Execute a security framework.
226
+
227
+ \b
228
+ Examples:
229
+ cartography-rules run all
230
+ cartography-rules run mitre-attack
231
+ cartography-rules run mitre-attack T1190
232
+ cartography-rules run mitre-attack T1190 aws_rds_public_access
233
+ """
234
+ # Validate framework
235
+ valid_frameworks = builtins.list(FRAMEWORKS.keys()) + ["all"]
236
+ if framework not in valid_frameworks:
237
+ typer.secho(
238
+ f"Error: Unknown framework '{framework}'", fg=typer.colors.RED, err=True
239
+ )
240
+ typer.echo(f"Available: {', '.join(valid_frameworks)}", err=True)
241
+ raise typer.Exit(1)
242
+
243
+ # Validate fact requires requirement
244
+ if fact and not requirement:
245
+ typer.secho(
246
+ "Error: Cannot specify fact without requirement",
247
+ fg=typer.colors.RED,
248
+ err=True,
249
+ )
250
+ raise typer.Exit(1)
251
+
252
+ # Validate filtering with 'all'
253
+ if framework == "all" and (requirement or fact):
254
+ typer.secho(
255
+ "Error: Cannot filter by requirement/fact when running all frameworks",
256
+ fg=typer.colors.RED,
257
+ err=True,
258
+ )
259
+ raise typer.Exit(1)
260
+
261
+ # Validate requirement exists
262
+ if requirement and framework != "all":
263
+ fw = FRAMEWORKS[framework]
264
+ req: Requirement | None = None
265
+ for r in fw.requirements:
266
+ if r.id.lower() == requirement.lower():
267
+ req = r
268
+ break
269
+
270
+ if not req:
271
+ typer.secho(
272
+ f"Error: Requirement '{requirement}' not found",
273
+ fg=typer.colors.RED,
274
+ err=True,
275
+ )
276
+ typer.echo("\nAvailable requirements:", err=True)
277
+ for r in fw.requirements:
278
+ typer.echo(f" {r.id}", err=True)
279
+ raise typer.Exit(1)
280
+
281
+ # Validate fact exists
282
+ if fact:
283
+ fact_found: Fact | None = None
284
+ for f in req.facts:
285
+ if f.id.lower() == fact.lower():
286
+ fact_found = f
287
+ break
288
+
289
+ if not fact_found:
290
+ typer.secho(
291
+ f"Error: Fact '{fact}' not found in requirement '{requirement}'",
292
+ fg=typer.colors.RED,
293
+ err=True,
294
+ )
295
+ typer.echo("\nAvailable facts:", err=True)
296
+ for f in req.facts:
297
+ typer.echo(f" {f.id}", err=True)
298
+ raise typer.Exit(1)
299
+
300
+ # Get password
301
+ password = None
302
+ if neo4j_password_prompt:
303
+ password = typer.prompt("Neo4j password", hide_input=True)
304
+ elif neo4j_password_env_var:
305
+ password = os.environ.get(neo4j_password_env_var)
306
+ else:
307
+ password = os.getenv("NEO4J_PASSWORD")
308
+ if not password:
309
+ password = typer.prompt("Neo4j password", hide_input=True)
310
+
311
+ # Determine frameworks to run
312
+ if framework == "all":
313
+ frameworks_to_run = builtins.list(FRAMEWORKS.keys())
314
+ else:
315
+ frameworks_to_run = [framework]
316
+
317
+ # Execute
318
+ try:
319
+ exit_code = run_frameworks(
320
+ frameworks_to_run,
321
+ uri,
322
+ user,
323
+ password,
324
+ database,
325
+ output.value,
326
+ requirement_filter=requirement,
327
+ fact_filter=fact,
328
+ )
329
+ raise typer.Exit(exit_code)
330
+ except KeyboardInterrupt:
331
+ raise typer.Exit(130)
332
+
333
+
334
+ def main():
335
+ """Entrypoint for cartography-rules CLI."""
336
+ logging.basicConfig(level=logging.INFO)
337
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
338
+ app()
339
+
340
+
341
+ if __name__ == "__main__":
342
+ main()
File without changes
@@ -0,0 +1,12 @@
1
+ """
2
+ Framework Registry
3
+
4
+ Central registry of all available security frameworks for Cartography Rules.
5
+ """
6
+
7
+ from cartography.rules.data.frameworks.mitre_attack import mitre_attack_framework
8
+
9
+ # Framework registry - all available frameworks
10
+ FRAMEWORKS = {
11
+ "mitre-attack": mitre_attack_framework,
12
+ }
@@ -0,0 +1,14 @@
1
+ # MITRE ATT&CK Framework
2
+ from cartography.rules.data.frameworks.mitre_attack.requirements.t1190_exploit_public_facing_application import (
3
+ t1190,
4
+ )
5
+ from cartography.rules.spec.model import Framework
6
+
7
+ mitre_attack_framework = Framework(
8
+ id="MITRE_ATTACK",
9
+ name="MITRE ATT&CK",
10
+ description="Comprehensive security assessment framework based on MITRE ATT&CK tactics and techniques",
11
+ version="1.0",
12
+ requirements=(t1190,),
13
+ source_url="https://attack.mitre.org/",
14
+ )
@@ -0,0 +1,135 @@
1
+ """
2
+ MITRE ATT&CK Framework
3
+ Security framework based on MITRE ATT&CK tactics and techniques
4
+ """
5
+
6
+ from cartography.rules.spec.model import Fact
7
+ from cartography.rules.spec.model import Module
8
+ from cartography.rules.spec.model import Requirement
9
+
10
+ # AWS
11
+ _aws_ec2_instance_internet_exposed = Fact(
12
+ id="aws_ec2_instance_internet_exposed",
13
+ name="Internet-Exposed EC2 Instances on Common Management Ports",
14
+ description=(
15
+ "EC2 instances exposed to the internet on ports 22, 3389, 3306, 5432, 6379, 9200, 27017"
16
+ ),
17
+ cypher_query="""
18
+ MATCH (a:AWSAccount)-[:RESOURCE]->(ec2:EC2Instance)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(rule:IpPermissionInbound)
19
+ MATCH (rule)<-[:MEMBER_OF_IP_RULE]-(ip:IpRange{range:'0.0.0.0/0'})
20
+ WHERE rule.fromport IN [22, 3389, 3306, 5432, 6379, 9200, 27017]
21
+ RETURN a.name AS account, ec2.instanceid AS instance, rule.fromport AS port, sg.groupid AS sg order by account, instance, port, sg
22
+ """,
23
+ cypher_visual_query="""
24
+ MATCH p=(a:AWSAccount)-[:RESOURCE]->(ec2:EC2Instance)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(rule:IpPermissionInbound)
25
+ MATCH p2=(rule)<-[:MEMBER_OF_IP_RULE]-(ip:IpRange{range:'0.0.0.0/0'})
26
+ WHERE rule.fromport IN [22, 3389, 3306, 5432, 6379, 9200, 27017]
27
+ RETURN *
28
+ """,
29
+ module=Module.AWS,
30
+ )
31
+ _aws_s3_public = Fact(
32
+ id="aws_s3_public",
33
+ name="Internet-Accessible S3 Storage Attack Surface",
34
+ description=("AWS S3 buckets accessible from the internet"),
35
+ cypher_query="""
36
+ MATCH (b:S3Bucket)
37
+ WHERE b.anonymous_access = true
38
+ OR (b.anonymous_actions IS NOT NULL AND size(b.anonymous_actions) > 0)
39
+ OR EXISTS {
40
+ MATCH (b)-[:POLICY_STATEMENT]->(stmt:S3PolicyStatement)
41
+ WHERE stmt.effect = 'Allow'
42
+ AND (stmt.principal = '*' OR stmt.principal CONTAINS 'AllUsers')
43
+ }
44
+ RETURN b.name AS bucket,
45
+ b.region AS region,
46
+ b.anonymous_access AS public_access,
47
+ b.anonymous_actions AS public_actions
48
+ """,
49
+ cypher_visual_query="""
50
+ MATCH (b:S3Bucket)
51
+ WHERE b.anonymous_access = true
52
+ OR (b.anonymous_actions IS NOT NULL AND size(b.anonymous_actions) > 0)
53
+ OR EXISTS {
54
+ MATCH (b)-[:POLICY_STATEMENT]->(stmt:S3PolicyStatement)
55
+ WHERE stmt.effect = 'Allow'
56
+ AND (stmt.principal = '*' OR stmt.principal CONTAINS 'AllUsers')
57
+ }
58
+ WITH b
59
+ OPTIONAL MATCH p=(b)-[:POLICY_STATEMENT]->(:S3PolicyStatement)
60
+ RETURN *
61
+ """,
62
+ module=Module.AWS,
63
+ )
64
+ _aws_rds_public_access = Fact(
65
+ id="aws_rds_public_access",
66
+ name="Internet-Accessible RDS Database Attack Surface",
67
+ description="AWS RDS instances accessible from the internet",
68
+ cypher_query="""
69
+ MATCH (rds:RDSInstance)
70
+ WHERE rds.publicly_accessible = true
71
+ RETURN rds.id AS instance_id,
72
+ rds.engine AS engine,
73
+ rds.db_instance_class AS instance_class,
74
+ rds.endpoint_address AS endpoint,
75
+ rds.endpoint_port AS port,
76
+ rds.region AS region,
77
+ rds.storage_encrypted AS encrypted
78
+ """,
79
+ cypher_visual_query="""
80
+ MATCH p1=(rds:RDSInstance{publicly_accessible: true})
81
+ OPTIONAL MATCH p2=(rds)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)
82
+ OPTIONAL MATCH p3=(rds)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(rule:IpPermissionInbound:IpRule)
83
+ OPTIONAL MATCH p4=(rds)-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(rule:IpPermissionInbound:IpRule)<-[:MEMBER_OF_IP_RULE]-(ip:IpRange)
84
+ RETURN *
85
+ """,
86
+ module=Module.AWS,
87
+ )
88
+
89
+ # Azure
90
+ _azure_storage_public_blob_access = Fact(
91
+ id="azure_storage_public_blob_access",
92
+ name="Azure Storage Accounts with Public Blob Containers",
93
+ description=(
94
+ "Azure Storage Accounts that have blob containers with public access. "
95
+ "If a storage blob container has public_access set to 'Container' or 'Blob', "
96
+ "it means that the container is publicly accessible."
97
+ ),
98
+ cypher_query="""
99
+ MATCH (sa:AzureStorageAccount)-[:USES]->(bs:AzureStorageBlobService)-[:CONTAINS]->(bc:AzureStorageBlobContainer)
100
+ WHERE bc.publicaccess IN ['Container', 'Blob']
101
+ RETURN sa.name AS storage_account,
102
+ sa.resourcegroup AS resource_group,
103
+ sa.location AS location,
104
+ bc.name AS container_name,
105
+ bc.publicaccess AS public_access
106
+ """,
107
+ cypher_visual_query="""
108
+ MATCH p=(sa:AzureStorageAccount)-[:USES]->(bs:AzureStorageBlobService)-[:CONTAINS]->(bc:AzureStorageBlobContainer)
109
+ WHERE bc.publicaccess IN ['Container', 'Blob']
110
+ RETURN *
111
+ """,
112
+ module=Module.AZURE,
113
+ )
114
+
115
+ t1190 = Requirement(
116
+ id="t1190",
117
+ name="Exploit Public-Facing Application",
118
+ description="Adversaries may attempt to take advantage of a weakness in an Internet-facing computer or program using software, data, or commands in order to cause unintended or unanticipated behavior.",
119
+ facts=(
120
+ # AWS
121
+ _aws_ec2_instance_internet_exposed,
122
+ _aws_s3_public,
123
+ _aws_rds_public_access,
124
+ # Azure
125
+ _azure_storage_public_blob_access,
126
+ ),
127
+ requirement_url="https://attack.mitre.org/techniques/T1190/",
128
+ # TODO: should we have a per-framework class represent the attributes?
129
+ attributes={
130
+ "tactic": "initial_access",
131
+ "technique_id": "T1190",
132
+ "services": ["ec2", "s3", "rds", "azure_storage"],
133
+ "providers": ["AWS", "AZURE"],
134
+ },
135
+ )
@@ -0,0 +1,46 @@
1
+ """
2
+ Output formatting utilities for Cartography rules.
3
+ """
4
+
5
+ import re
6
+ from urllib.parse import quote
7
+
8
+
9
+ def _generate_neo4j_browser_url(neo4j_uri: str, cypher_query: str) -> str:
10
+ """Generate a clickable Neo4j Browser URL with pre-populated query."""
11
+ # Handle different Neo4j URI protocols
12
+ if neo4j_uri.startswith("bolt://"):
13
+ browser_uri = neo4j_uri.replace("bolt://", "http://", 1)
14
+ elif neo4j_uri.startswith("bolt+s://"):
15
+ browser_uri = neo4j_uri.replace("bolt+s://", "https://", 1)
16
+ elif neo4j_uri.startswith("bolt+ssc://"):
17
+ browser_uri = neo4j_uri.replace("bolt+ssc://", "https://", 1)
18
+ elif neo4j_uri.startswith("neo4j://"):
19
+ browser_uri = neo4j_uri.replace("neo4j://", "http://", 1)
20
+ elif neo4j_uri.startswith("neo4j+s://"):
21
+ browser_uri = neo4j_uri.replace("neo4j+s://", "https://", 1)
22
+ elif neo4j_uri.startswith("neo4j+ssc://"):
23
+ browser_uri = neo4j_uri.replace("neo4j+ssc://", "https://", 1)
24
+ else:
25
+ browser_uri = neo4j_uri
26
+
27
+ # Handle port mapping for local instances
28
+ if ":7687" in browser_uri and (
29
+ "localhost" in browser_uri or "127.0.0.1" in browser_uri
30
+ ):
31
+ browser_uri = browser_uri.replace(":7687", ":7474")
32
+
33
+ # For Neo4j Aura (cloud), remove the port as it uses standard HTTPS port
34
+ if ".databases.neo4j.io" in browser_uri:
35
+ # Remove any port number for Aura URLs
36
+ browser_uri = re.sub(r":\d+", "", browser_uri)
37
+
38
+ # Ensure the URL ends properly
39
+ if not browser_uri.endswith("/"):
40
+ browser_uri += "/"
41
+
42
+ # URL encode the cypher query
43
+ encoded_query = quote(cypher_query.strip())
44
+
45
+ # Construct the Neo4j Browser URL with pre-populated query
46
+ return f"{browser_uri}browser/?cmd=edit&arg={encoded_query}"