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.
- cartography/_version.py +2 -2
- cartography/client/core/tx.py +1 -1
- cartography/intel/aws/ecr_image_layers.py +664 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/azure/__init__.py +8 -0
- cartography/intel/azure/resource_groups.py +82 -0
- cartography/models/aws/ecr/image.py +21 -0
- cartography/models/aws/ecr/image_layer.py +107 -0
- cartography/models/azure/resource_groups.py +52 -0
- cartography/rules/README.md +1 -0
- cartography/rules/__init__.py +0 -0
- cartography/rules/cli.py +342 -0
- cartography/rules/data/__init__.py +0 -0
- cartography/rules/data/frameworks/__init__.py +12 -0
- cartography/rules/data/frameworks/mitre_attack/__init__.py +14 -0
- cartography/rules/data/frameworks/mitre_attack/requirements/__init__.py +0 -0
- cartography/rules/data/frameworks/mitre_attack/requirements/t1190_exploit_public_facing_application/__init__.py +135 -0
- cartography/rules/formatters.py +46 -0
- cartography/rules/runners.py +338 -0
- cartography/rules/spec/__init__.py +0 -0
- cartography/rules/spec/model.py +88 -0
- cartography/rules/spec/result.py +46 -0
- {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/METADATA +19 -4
- {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/RECORD +28 -11
- {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/entry_points.txt +1 -0
- {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/WHEEL +0 -0
- {cartography-0.115.0.dist-info → cartography-0.116.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
cartography/rules/cli.py
ADDED
|
@@ -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
|
+
)
|
|
File without changes
|