cartography 0.115.0__py3-none-any.whl → 0.116.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 (28) hide show
  1. cartography/_version.py +2 -2
  2. cartography/client/core/tx.py +1 -1
  3. cartography/intel/aws/ecr_image_layers.py +664 -0
  4. cartography/intel/aws/resources.py +2 -0
  5. cartography/intel/azure/__init__.py +8 -0
  6. cartography/intel/azure/resource_groups.py +82 -0
  7. cartography/models/aws/ecr/image.py +21 -0
  8. cartography/models/aws/ecr/image_layer.py +107 -0
  9. cartography/models/azure/resource_groups.py +52 -0
  10. cartography/rules/README.md +1 -0
  11. cartography/rules/__init__.py +0 -0
  12. cartography/rules/cli.py +342 -0
  13. cartography/rules/data/__init__.py +0 -0
  14. cartography/rules/data/frameworks/__init__.py +12 -0
  15. cartography/rules/data/frameworks/mitre_attack/__init__.py +14 -0
  16. cartography/rules/data/frameworks/mitre_attack/requirements/__init__.py +0 -0
  17. cartography/rules/data/frameworks/mitre_attack/requirements/t1190_exploit_public_facing_application/__init__.py +135 -0
  18. cartography/rules/formatters.py +46 -0
  19. cartography/rules/runners.py +338 -0
  20. cartography/rules/spec/__init__.py +0 -0
  21. cartography/rules/spec/model.py +88 -0
  22. cartography/rules/spec/result.py +46 -0
  23. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/METADATA +19 -4
  24. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/RECORD +28 -11
  25. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/entry_points.txt +1 -0
  26. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/WHEEL +0 -0
  27. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/licenses/LICENSE +0 -0
  28. {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,7 @@ from . import compute
12
12
  from . import cosmosdb
13
13
  from . import functions
14
14
  from . import logic_apps
15
+ from . import resource_groups
15
16
  from . import sql
16
17
  from . import storage
17
18
  from . import subscription
@@ -78,6 +79,13 @@ def _sync_one_subscription(
78
79
  update_tag,
79
80
  common_job_parameters,
80
81
  )
82
+ resource_groups.sync(
83
+ neo4j_session,
84
+ credentials,
85
+ subscription_id,
86
+ update_tag,
87
+ common_job_parameters,
88
+ )
81
89
 
82
90
 
83
91
  def _sync_tenant(
@@ -0,0 +1,82 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ import neo4j
5
+ from azure.core.exceptions import ClientAuthenticationError
6
+ from azure.core.exceptions import HttpResponseError
7
+ from azure.mgmt.resource import ResourceManagementClient
8
+
9
+ from cartography.client.core.tx import load
10
+ from cartography.graph.job import GraphJob
11
+ from cartography.models.azure.resource_groups import AzureResourceGroupSchema
12
+ from cartography.util import timeit
13
+
14
+ from .util.credentials import Credentials
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @timeit
20
+ def get_resource_groups(credentials: Credentials, subscription_id: str) -> list[dict]:
21
+ try:
22
+ client = ResourceManagementClient(credentials.credential, subscription_id)
23
+ return [rg.as_dict() for rg in client.resource_groups.list()]
24
+ except (ClientAuthenticationError, HttpResponseError) as e:
25
+ logger.warning(
26
+ f"Failed to get Resource Groups for subscription {subscription_id}: {str(e)}"
27
+ )
28
+ return []
29
+
30
+
31
+ @timeit
32
+ def transform_resource_groups(resource_groups_response: list[dict]) -> list[dict]:
33
+ transformed_groups: list[dict[str, Any]] = []
34
+ for rg in resource_groups_response:
35
+ transformed_group = {
36
+ "id": rg.get("id"),
37
+ "name": rg.get("name"),
38
+ "location": rg.get("location"),
39
+ "provisioning_state": rg.get("properties", {}).get("provisioning_state"),
40
+ }
41
+ transformed_groups.append(transformed_group)
42
+ return transformed_groups
43
+
44
+
45
+ @timeit
46
+ def load_resource_groups(
47
+ neo4j_session: neo4j.Session,
48
+ data: list[dict[str, Any]],
49
+ subscription_id: str,
50
+ update_tag: int,
51
+ ) -> None:
52
+ load(
53
+ neo4j_session,
54
+ AzureResourceGroupSchema(),
55
+ data,
56
+ lastupdated=update_tag,
57
+ AZURE_SUBSCRIPTION_ID=subscription_id,
58
+ )
59
+
60
+
61
+ @timeit
62
+ def cleanup_resource_groups(
63
+ neo4j_session: neo4j.Session, common_job_parameters: dict
64
+ ) -> None:
65
+ GraphJob.from_node_schema(AzureResourceGroupSchema(), common_job_parameters).run(
66
+ neo4j_session
67
+ )
68
+
69
+
70
+ @timeit
71
+ def sync(
72
+ neo4j_session: neo4j.Session,
73
+ credentials: Credentials,
74
+ subscription_id: str,
75
+ update_tag: int,
76
+ common_job_parameters: dict,
77
+ ) -> None:
78
+ logger.info(f"Syncing Azure Resource Groups for subscription {subscription_id}.")
79
+ raw_groups = get_resource_groups(credentials, subscription_id)
80
+ transformed_groups = transform_resource_groups(raw_groups)
81
+ load_resource_groups(neo4j_session, transformed_groups, subscription_id, update_tag)
82
+ cleanup_resource_groups(neo4j_session, common_job_parameters)
@@ -7,6 +7,7 @@ from cartography.models.core.relationships import CartographyRelProperties
7
7
  from cartography.models.core.relationships import CartographyRelSchema
8
8
  from cartography.models.core.relationships import LinkDirection
9
9
  from cartography.models.core.relationships import make_target_node_matcher
10
+ from cartography.models.core.relationships import OtherRelationships
10
11
  from cartography.models.core.relationships import TargetNodeMatcher
11
12
 
12
13
 
@@ -16,6 +17,7 @@ class ECRImageNodeProperties(CartographyNodeProperties):
16
17
  digest: PropertyRef = PropertyRef("imageDigest")
17
18
  region: PropertyRef = PropertyRef("Region", set_in_kwargs=True)
18
19
  lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
20
+ layer_diff_ids: PropertyRef = PropertyRef("layer_diff_ids")
19
21
 
20
22
 
21
23
  @dataclass(frozen=True)
@@ -34,8 +36,27 @@ class ECRImageToAWSAccountRel(CartographyRelSchema):
34
36
  properties: ECRImageToAWSAccountRelProperties = ECRImageToAWSAccountRelProperties()
35
37
 
36
38
 
39
+ @dataclass(frozen=True)
40
+ class ECRImageHasLayerRelProperties(CartographyRelProperties):
41
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ECRImageHasLayerRel(CartographyRelSchema):
46
+ target_node_label: str = "ECRImageLayer"
47
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
48
+ {"diff_id": PropertyRef("layer_diff_ids", one_to_many=True)},
49
+ )
50
+ direction: LinkDirection = LinkDirection.OUTWARD
51
+ rel_label: str = "HAS_LAYER"
52
+ properties: ECRImageHasLayerRelProperties = ECRImageHasLayerRelProperties()
53
+
54
+
37
55
  @dataclass(frozen=True)
38
56
  class ECRImageSchema(CartographyNodeSchema):
39
57
  label: str = "ECRImage"
40
58
  properties: ECRImageNodeProperties = ECRImageNodeProperties()
41
59
  sub_resource_relationship: ECRImageToAWSAccountRel = ECRImageToAWSAccountRel()
60
+ other_relationships: OtherRelationships = OtherRelationships(
61
+ [ECRImageHasLayerRel()],
62
+ )
@@ -0,0 +1,107 @@
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 ECRImageLayerNodeProperties(CartographyNodeProperties):
17
+ id: PropertyRef = PropertyRef("diff_id")
18
+ diff_id: PropertyRef = PropertyRef("diff_id")
19
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
20
+ is_empty: PropertyRef = PropertyRef("is_empty")
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ECRImageLayerToAWSAccountRelProperties(CartographyRelProperties):
25
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ECRImageLayerToAWSAccountRel(CartographyRelSchema):
30
+ target_node_label: str = "AWSAccount"
31
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
32
+ {"id": PropertyRef("AWS_ID", set_in_kwargs=True)}
33
+ )
34
+ direction: LinkDirection = LinkDirection.INWARD
35
+ rel_label: str = "RESOURCE"
36
+ properties: ECRImageLayerToAWSAccountRelProperties = (
37
+ ECRImageLayerToAWSAccountRelProperties()
38
+ )
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ECRImageLayerToNextRelProperties(CartographyRelProperties):
43
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ECRImageLayerToNextRel(CartographyRelSchema):
48
+ target_node_label: str = "ECRImageLayer"
49
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
50
+ {"diff_id": PropertyRef("next_diff_ids", one_to_many=True)}
51
+ )
52
+ direction: LinkDirection = LinkDirection.OUTWARD
53
+ rel_label: str = "NEXT"
54
+ properties: ECRImageLayerToNextRelProperties = ECRImageLayerToNextRelProperties()
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ECRImageLayerHeadOfImageRelProperties(CartographyRelProperties):
59
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ECRImageLayerHeadOfImageRel(CartographyRelSchema):
64
+ target_node_label: str = "ECRImage"
65
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
66
+ {"id": PropertyRef("head_image_ids", one_to_many=True)}
67
+ )
68
+ direction: LinkDirection = LinkDirection.INWARD
69
+ rel_label: str = "HEAD"
70
+ properties: ECRImageLayerHeadOfImageRelProperties = (
71
+ ECRImageLayerHeadOfImageRelProperties()
72
+ )
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class ECRImageLayerTailOfImageRelProperties(CartographyRelProperties):
77
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ECRImageLayerTailOfImageRel(CartographyRelSchema):
82
+ target_node_label: str = "ECRImage"
83
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
84
+ {"id": PropertyRef("tail_image_ids", one_to_many=True)}
85
+ )
86
+ direction: LinkDirection = LinkDirection.INWARD
87
+ rel_label: str = "TAIL"
88
+ properties: ECRImageLayerTailOfImageRelProperties = (
89
+ ECRImageLayerTailOfImageRelProperties()
90
+ )
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class ECRImageLayerSchema(CartographyNodeSchema):
95
+ label: str = "ECRImageLayer"
96
+ properties: ECRImageLayerNodeProperties = ECRImageLayerNodeProperties()
97
+ sub_resource_relationship: ECRImageLayerToAWSAccountRel = (
98
+ ECRImageLayerToAWSAccountRel()
99
+ )
100
+ other_relationships: OtherRelationships = OtherRelationships(
101
+ [
102
+ ECRImageLayerToNextRel(),
103
+ ECRImageLayerHeadOfImageRel(),
104
+ ECRImageLayerTailOfImageRel(),
105
+ ]
106
+ )
107
+ extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(["ImageLayer"])
@@ -0,0 +1,52 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+
4
+ from cartography.models.core.common import PropertyRef
5
+ from cartography.models.core.nodes import CartographyNodeProperties
6
+ from cartography.models.core.nodes import CartographyNodeSchema
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 TargetNodeMatcher
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ # --- Node Definitions ---
17
+ @dataclass(frozen=True)
18
+ class AzureResourceGroupProperties(CartographyNodeProperties):
19
+ id: PropertyRef = PropertyRef("id")
20
+ name: PropertyRef = PropertyRef("name")
21
+ location: PropertyRef = PropertyRef("location")
22
+ provisioning_state: PropertyRef = PropertyRef("provisioning_state")
23
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
24
+
25
+
26
+ # --- Relationship Definitions ---
27
+ @dataclass(frozen=True)
28
+ class AzureResourceGroupToSubscriptionRelProperties(CartographyRelProperties):
29
+ lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AzureResourceGroupToSubscriptionRel(CartographyRelSchema):
34
+ target_node_label: str = "AzureSubscription"
35
+ target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
36
+ {"id": PropertyRef("AZURE_SUBSCRIPTION_ID", set_in_kwargs=True)},
37
+ )
38
+ direction: LinkDirection = LinkDirection.INWARD
39
+ rel_label: str = "RESOURCE"
40
+ properties: AzureResourceGroupToSubscriptionRelProperties = (
41
+ AzureResourceGroupToSubscriptionRelProperties()
42
+ )
43
+
44
+
45
+ # --- Main Schema ---
46
+ @dataclass(frozen=True)
47
+ class AzureResourceGroupSchema(CartographyNodeSchema):
48
+ label: str = "AzureResourceGroup"
49
+ properties: AzureResourceGroupProperties = AzureResourceGroupProperties()
50
+ sub_resource_relationship: AzureResourceGroupToSubscriptionRel = (
51
+ AzureResourceGroupToSubscriptionRel()
52
+ )
@@ -0,0 +1 @@
1
+ See [the rules docs](https://cartography-cncf.github.io/cartography/usage/rules.html) for how Cartography develops and approaches security rules.
File without changes
@@ -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
+ )