cartography 0.116.1__py3-none-any.whl → 0.117.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.
- cartography/_version.py +2 -2
- cartography/client/core/tx.py +23 -2
- cartography/intel/aws/ecr_image_layers.py +174 -21
- cartography/intel/azure/__init__.py +16 -0
- cartography/intel/azure/container_instances.py +95 -0
- cartography/intel/azure/data_lake.py +124 -0
- cartography/intel/github/teams.py +3 -3
- cartography/models/aws/ecr/image.py +30 -1
- cartography/models/azure/container_instance.py +55 -0
- cartography/models/azure/data_lake_filesystem.py +51 -0
- cartography/rules/cli.py +8 -6
- cartography/rules/data/frameworks/mitre_attack/__init__.py +7 -1
- cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py +317 -0
- cartography/rules/data/frameworks/mitre_attack/requirements/t1190_exploit_public_facing_application/__init__.py +1 -0
- cartography/rules/spec/model.py +13 -0
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/METADATA +3 -2
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/RECORD +21 -16
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/WHEEL +0 -0
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/entry_points.txt +0 -0
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/licenses/LICENSE +0 -0
- {cartography-0.116.1.dist-info → cartography-0.117.0.dist-info}/top_level.txt +0 -0
|
@@ -52,11 +52,40 @@ class ECRImageHasLayerRel(CartographyRelSchema):
|
|
|
52
52
|
properties: ECRImageHasLayerRelProperties = ECRImageHasLayerRelProperties()
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ECRImageToParentImageRelProperties(CartographyRelProperties):
|
|
57
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
58
|
+
from_attestation: PropertyRef = PropertyRef("from_attestation")
|
|
59
|
+
parent_image_uri: PropertyRef = PropertyRef("parent_image_uri")
|
|
60
|
+
confidence: PropertyRef = PropertyRef("confidence")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class ECRImageToParentImageRel(CartographyRelSchema):
|
|
65
|
+
"""
|
|
66
|
+
Relationship from an ECRImage to its parent ECRImage (BUILT_FROM).
|
|
67
|
+
This relationship is created when provenance attestations explicitly specify the parent image.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
target_node_label: str = "ECRImage"
|
|
71
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
72
|
+
{"digest": PropertyRef("parent_image_digest")},
|
|
73
|
+
)
|
|
74
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
75
|
+
rel_label: str = "BUILT_FROM"
|
|
76
|
+
properties: ECRImageToParentImageRelProperties = (
|
|
77
|
+
ECRImageToParentImageRelProperties()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
55
81
|
@dataclass(frozen=True)
|
|
56
82
|
class ECRImageSchema(CartographyNodeSchema):
|
|
57
83
|
label: str = "ECRImage"
|
|
58
84
|
properties: ECRImageNodeProperties = ECRImageNodeProperties()
|
|
59
85
|
sub_resource_relationship: ECRImageToAWSAccountRel = ECRImageToAWSAccountRel()
|
|
60
86
|
other_relationships: OtherRelationships = OtherRelationships(
|
|
61
|
-
[
|
|
87
|
+
[
|
|
88
|
+
ECRImageHasLayerRel(),
|
|
89
|
+
ECRImageToParentImageRel(),
|
|
90
|
+
],
|
|
62
91
|
)
|
|
@@ -0,0 +1,55 @@
|
|
|
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 AzureContainerInstanceProperties(CartographyNodeProperties):
|
|
19
|
+
id: PropertyRef = PropertyRef("id")
|
|
20
|
+
name: PropertyRef = PropertyRef("name")
|
|
21
|
+
location: PropertyRef = PropertyRef("location")
|
|
22
|
+
type: PropertyRef = PropertyRef("type")
|
|
23
|
+
provisioning_state: PropertyRef = PropertyRef("provisioning_state")
|
|
24
|
+
ip_address: PropertyRef = PropertyRef("ip_address")
|
|
25
|
+
os_type: PropertyRef = PropertyRef("os_type")
|
|
26
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --- Relationship Definitions ---
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class AzureContainerInstanceToSubscriptionRelProperties(CartographyRelProperties):
|
|
32
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AzureContainerInstanceToSubscriptionRel(CartographyRelSchema):
|
|
37
|
+
target_node_label: str = "AzureSubscription"
|
|
38
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
39
|
+
{"id": PropertyRef("AZURE_SUBSCRIPTION_ID", set_in_kwargs=True)},
|
|
40
|
+
)
|
|
41
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
42
|
+
rel_label: str = "RESOURCE"
|
|
43
|
+
properties: AzureContainerInstanceToSubscriptionRelProperties = (
|
|
44
|
+
AzureContainerInstanceToSubscriptionRelProperties()
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- Main Schema ---
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class AzureContainerInstanceSchema(CartographyNodeSchema):
|
|
51
|
+
label: str = "AzureContainerInstance"
|
|
52
|
+
properties: AzureContainerInstanceProperties = AzureContainerInstanceProperties()
|
|
53
|
+
sub_resource_relationship: AzureContainerInstanceToSubscriptionRel = (
|
|
54
|
+
AzureContainerInstanceToSubscriptionRel()
|
|
55
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AzureDataLakeFileSystemProperties(CartographyNodeProperties):
|
|
18
|
+
id: PropertyRef = PropertyRef("id")
|
|
19
|
+
name: PropertyRef = PropertyRef("name")
|
|
20
|
+
public_access: PropertyRef = PropertyRef("public_access")
|
|
21
|
+
last_modified_time: PropertyRef = PropertyRef("last_modified_time")
|
|
22
|
+
has_immutability_policy: PropertyRef = PropertyRef("has_immutability_policy")
|
|
23
|
+
has_legal_hold: PropertyRef = PropertyRef("has_legal_hold")
|
|
24
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class AzureDataLakeFileSystemToStorageAccountRelProperties(CartographyRelProperties):
|
|
29
|
+
lastupdated: PropertyRef = PropertyRef("lastupdated", set_in_kwargs=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class AzureDataLakeFileSystemToStorageAccountRel(CartographyRelSchema):
|
|
34
|
+
target_node_label: str = "AzureStorageAccount"
|
|
35
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
36
|
+
{"id": PropertyRef("STORAGE_ACCOUNT_ID", set_in_kwargs=True)},
|
|
37
|
+
)
|
|
38
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
39
|
+
rel_label: str = "CONTAINS"
|
|
40
|
+
properties: AzureDataLakeFileSystemToStorageAccountRelProperties = (
|
|
41
|
+
AzureDataLakeFileSystemToStorageAccountRelProperties()
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class AzureDataLakeFileSystemSchema(CartographyNodeSchema):
|
|
47
|
+
label: str = "AzureDataLakeFileSystem"
|
|
48
|
+
properties: AzureDataLakeFileSystemProperties = AzureDataLakeFileSystemProperties()
|
|
49
|
+
sub_resource_relationship: AzureDataLakeFileSystemToStorageAccountRel = (
|
|
50
|
+
AzureDataLakeFileSystemToStorageAccountRel()
|
|
51
|
+
)
|
cartography/rules/cli.py
CHANGED
|
@@ -47,19 +47,21 @@ def complete_frameworks_with_all(incomplete: str) -> Generator[str, None, None]:
|
|
|
47
47
|
|
|
48
48
|
def complete_requirements(
|
|
49
49
|
ctx: typer.Context, incomplete: str
|
|
50
|
-
) -> Generator[str, None, None]:
|
|
51
|
-
"""Autocomplete requirement IDs based on selected framework."""
|
|
50
|
+
) -> Generator[tuple[str, str], None, None]:
|
|
51
|
+
"""Autocomplete requirement IDs with descriptions based on selected framework."""
|
|
52
52
|
framework = ctx.params.get("framework")
|
|
53
53
|
if not framework or framework not in FRAMEWORKS:
|
|
54
54
|
return
|
|
55
55
|
|
|
56
56
|
for req in FRAMEWORKS[framework].requirements:
|
|
57
57
|
if req.id.lower().startswith(incomplete.lower()):
|
|
58
|
-
yield req.id
|
|
58
|
+
yield (req.id, req.name)
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def complete_facts(
|
|
62
|
-
|
|
61
|
+
def complete_facts(
|
|
62
|
+
ctx: typer.Context, incomplete: str
|
|
63
|
+
) -> Generator[tuple[str, str], None, None]:
|
|
64
|
+
"""Autocomplete fact IDs with descriptions based on selected framework and requirement."""
|
|
63
65
|
framework = ctx.params.get("framework")
|
|
64
66
|
requirement_id = ctx.params.get("requirement")
|
|
65
67
|
|
|
@@ -73,7 +75,7 @@ def complete_facts(ctx: typer.Context, incomplete: str) -> Generator[str, None,
|
|
|
73
75
|
if req.id.lower() == requirement_id.lower():
|
|
74
76
|
for fact in req.facts:
|
|
75
77
|
if fact.id.lower().startswith(incomplete.lower()):
|
|
76
|
-
yield fact.id
|
|
78
|
+
yield (fact.id, fact.name)
|
|
77
79
|
break
|
|
78
80
|
|
|
79
81
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
# MITRE ATT&CK Framework
|
|
2
|
+
from cartography.rules.data.frameworks.mitre_attack.requirements.t1098_account_manipulation import (
|
|
3
|
+
t1098,
|
|
4
|
+
)
|
|
2
5
|
from cartography.rules.data.frameworks.mitre_attack.requirements.t1190_exploit_public_facing_application import (
|
|
3
6
|
t1190,
|
|
4
7
|
)
|
|
@@ -9,6 +12,9 @@ mitre_attack_framework = Framework(
|
|
|
9
12
|
name="MITRE ATT&CK",
|
|
10
13
|
description="Comprehensive security assessment framework based on MITRE ATT&CK tactics and techniques",
|
|
11
14
|
version="1.0",
|
|
12
|
-
requirements=(
|
|
15
|
+
requirements=(
|
|
16
|
+
t1098,
|
|
17
|
+
t1190,
|
|
18
|
+
),
|
|
13
19
|
source_url="https://attack.mitre.org/",
|
|
14
20
|
)
|
cartography/rules/data/frameworks/mitre_attack/requirements/t1098_account_manipulation/__init__.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from cartography.rules.spec.model import Fact
|
|
2
|
+
from cartography.rules.spec.model import Module
|
|
3
|
+
from cartography.rules.spec.model import Requirement
|
|
4
|
+
|
|
5
|
+
# AWS
|
|
6
|
+
_aws_account_manipulation_permissions = Fact(
|
|
7
|
+
id="aws_account_manipulation_permissions",
|
|
8
|
+
name="IAM Principals with Account Creation and Modification Permissions",
|
|
9
|
+
description=(
|
|
10
|
+
"AWS IAM users and roles with permissions to create, modify, or delete IAM "
|
|
11
|
+
"accounts and their associated policies."
|
|
12
|
+
),
|
|
13
|
+
cypher_query="""
|
|
14
|
+
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
15
|
+
MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
16
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
17
|
+
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
18
|
+
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
19
|
+
AND stmt.effect = 'Allow'
|
|
20
|
+
WITH a, principal, stmt,
|
|
21
|
+
// Return labels that are not the general "AWSPrincipal" label
|
|
22
|
+
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
23
|
+
// Define the list of IAM actions to match on
|
|
24
|
+
[p IN ['iam:Create','iam:Attach','iam:Put','iam:Update','iam:Add'] |
|
|
25
|
+
p] AS patterns
|
|
26
|
+
WITH a, principal, principal_type, stmt,
|
|
27
|
+
// Filter on the desired IAM actions
|
|
28
|
+
[action IN stmt.action
|
|
29
|
+
WHERE ANY(prefix IN patterns WHERE action STARTS WITH prefix)
|
|
30
|
+
OR action = 'iam:*'
|
|
31
|
+
OR action = '*'
|
|
32
|
+
] AS matched_actions
|
|
33
|
+
// Return only statement actions that we matched on
|
|
34
|
+
WHERE size(matched_actions) > 0
|
|
35
|
+
UNWIND matched_actions AS action
|
|
36
|
+
RETURN DISTINCT a.name AS account,
|
|
37
|
+
principal.name AS principal_name,
|
|
38
|
+
principal.arn AS principal_arn,
|
|
39
|
+
principal_type,
|
|
40
|
+
collect(action) as action,
|
|
41
|
+
stmt.resource AS resource
|
|
42
|
+
ORDER BY account, principal_name
|
|
43
|
+
""",
|
|
44
|
+
cypher_visual_query="""
|
|
45
|
+
MATCH p = (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
46
|
+
MATCH p1 = (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
47
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
48
|
+
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
49
|
+
AND NOT principal.name = 'OrganizationAccountAccessRole'
|
|
50
|
+
AND stmt.effect = 'Allow'
|
|
51
|
+
AND ANY(action IN stmt.action WHERE
|
|
52
|
+
action STARTS WITH 'iam:Create'
|
|
53
|
+
OR action STARTS WITH 'iam:Attach'
|
|
54
|
+
OR action STARTS WITH 'iam:Put'
|
|
55
|
+
OR action STARTS WITH 'iam:Update'
|
|
56
|
+
OR action STARTS WITH 'iam:Add'
|
|
57
|
+
OR action = 'iam:*'
|
|
58
|
+
OR action = '*'
|
|
59
|
+
)
|
|
60
|
+
RETURN *
|
|
61
|
+
""",
|
|
62
|
+
module=Module.AWS,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_aws_trust_relationship_manipulation = Fact(
|
|
66
|
+
id="aws_trust_relationship_manipulation",
|
|
67
|
+
name="Roles with Cross-Account Trust Relationship Modification Capabilities",
|
|
68
|
+
description=(
|
|
69
|
+
"AWS IAM principals with permissions to modify role trust policies "
|
|
70
|
+
"(specifically AssumeRolePolicyDocuments)."
|
|
71
|
+
),
|
|
72
|
+
cypher_query="""
|
|
73
|
+
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
74
|
+
MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
75
|
+
OPTIONAL MATCH (groupmember:AWSUser)
|
|
76
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
77
|
+
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
78
|
+
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
79
|
+
AND stmt.effect = 'Allow'
|
|
80
|
+
WITH a, principal, stmt,
|
|
81
|
+
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
82
|
+
['iam:UpdateAssumeRolePolicy','iam:CreateRole'] AS patterns
|
|
83
|
+
WITH a, principal, principal_type, stmt,
|
|
84
|
+
[action IN stmt.action
|
|
85
|
+
WHERE ANY(p IN patterns WHERE action = p)
|
|
86
|
+
OR action = 'iam:*' OR action = '*'
|
|
87
|
+
] AS matched_actions
|
|
88
|
+
WHERE size(matched_actions) > 0
|
|
89
|
+
UNWIND matched_actions AS action
|
|
90
|
+
RETURN DISTINCT a.name AS account,
|
|
91
|
+
principal.name AS principal_name,
|
|
92
|
+
principal.arn AS principal_arn,
|
|
93
|
+
principal_type,
|
|
94
|
+
collect(distinct action) as action,
|
|
95
|
+
stmt.resource AS resource
|
|
96
|
+
ORDER BY account, principal_name
|
|
97
|
+
""",
|
|
98
|
+
cypher_visual_query="""
|
|
99
|
+
MATCH p = (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
100
|
+
MATCH p1 = (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
101
|
+
MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
102
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
103
|
+
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
104
|
+
AND stmt.effect = 'Allow'
|
|
105
|
+
RETURN *
|
|
106
|
+
""",
|
|
107
|
+
module=Module.AWS,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_aws_service_account_manipulation_via_ec2 = Fact(
|
|
111
|
+
id="aws_service_account_manipulation_via_ec2",
|
|
112
|
+
name="Service Resources with Account Manipulation Through Instance Profiles",
|
|
113
|
+
description=(
|
|
114
|
+
"AWS EC2 instances with attached IAM roles that can manipulate other AWS accounts. "
|
|
115
|
+
"Also indicates whether the instance is internet-exposed."
|
|
116
|
+
),
|
|
117
|
+
cypher_query="""
|
|
118
|
+
MATCH (a:AWSAccount)-[:RESOURCE]->(ec2:EC2Instance)
|
|
119
|
+
MATCH (ec2)-[:INSTANCE_PROFILE]->(profile:AWSInstanceProfile)
|
|
120
|
+
MATCH (profile)-[:ASSOCIATED_WITH]->(role:AWSRole)
|
|
121
|
+
MATCH (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
122
|
+
WHERE stmt.effect = 'Allow'
|
|
123
|
+
WITH a, ec2, role,
|
|
124
|
+
// Define the list of IAM actions to match on
|
|
125
|
+
['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update'] AS patterns,
|
|
126
|
+
// Filter on the desired IAM actions
|
|
127
|
+
[action IN stmt.action
|
|
128
|
+
WHERE ANY(p IN ['iam:Create','iam:Attach','iam:Put','iam:Update', 'iam:Add'] WHERE action STARTS WITH p)
|
|
129
|
+
OR action = 'iam:*'
|
|
130
|
+
OR action = '*'
|
|
131
|
+
] AS actions
|
|
132
|
+
// For the instances that are internet open, include the SG and rules
|
|
133
|
+
OPTIONAL MATCH (ec2{exposed_internet:True})-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(ip:IpPermissionInbound)
|
|
134
|
+
// Return only statement actions that we matched on
|
|
135
|
+
WHERE size(actions) > 0
|
|
136
|
+
UNWIND actions AS flat_action
|
|
137
|
+
WITH a, ec2, role, sg, ip,
|
|
138
|
+
collect(DISTINCT flat_action) AS actions
|
|
139
|
+
RETURN DISTINCT a.name AS account,
|
|
140
|
+
a.id as account_id,
|
|
141
|
+
ec2.instanceid AS instance_id,
|
|
142
|
+
ec2.exposed_internet AS internet_accessible,
|
|
143
|
+
ec2.publicipaddress as public_ip_address,
|
|
144
|
+
role.name AS role_name,
|
|
145
|
+
collect(actions) as action,
|
|
146
|
+
ip.fromport as from_port,
|
|
147
|
+
ip.toport as to_port
|
|
148
|
+
ORDER BY account, instance_id, internet_accessible, from_port
|
|
149
|
+
""",
|
|
150
|
+
cypher_visual_query="""
|
|
151
|
+
MATCH p = (a:AWSAccount)-[:RESOURCE]->(ec2:EC2Instance)
|
|
152
|
+
MATCH p1 = (ec2)-[:INSTANCE_PROFILE]->(profile:AWSInstanceProfile)
|
|
153
|
+
MATCH p2 = (profile)-[:ASSOCIATED_WITH]->(role:AWSRole)
|
|
154
|
+
MATCH p3 = (role)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
155
|
+
WHERE stmt.effect = 'Allow'
|
|
156
|
+
AND ANY(action IN stmt.action WHERE
|
|
157
|
+
action STARTS WITH 'iam:Create'
|
|
158
|
+
OR action STARTS WITH 'iam:Attach'
|
|
159
|
+
OR action STARTS WITH 'iam:Put'
|
|
160
|
+
OR action STARTS WITH 'iam:Update'
|
|
161
|
+
OR action STARTS WITH 'iam:Add'
|
|
162
|
+
OR action = 'iam:*'
|
|
163
|
+
OR action = '*'
|
|
164
|
+
)
|
|
165
|
+
WITH p, p1, p2, p3, ec2
|
|
166
|
+
// Include the SG and rules for the instances that are internet open
|
|
167
|
+
MATCH p4=(ec2{exposed_internet: true})-[:MEMBER_OF_EC2_SECURITY_GROUP]->(sg:EC2SecurityGroup)<-[:MEMBER_OF_EC2_SECURITY_GROUP]-(ip:IpPermissionInbound)
|
|
168
|
+
RETURN *
|
|
169
|
+
""",
|
|
170
|
+
module=Module.AWS,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
_aws_service_account_manipulation_via_lambda = Fact(
|
|
174
|
+
id="aws_service_account_manipulation",
|
|
175
|
+
name="Service Resources with Account Manipulation Through Lambda Roles",
|
|
176
|
+
description=(
|
|
177
|
+
"AWS Lambda functions with IAM roles that can manipulate other AWS accounts."
|
|
178
|
+
),
|
|
179
|
+
cypher_query="""
|
|
180
|
+
// Find Lambda functions with account manipulation capabilities
|
|
181
|
+
MATCH (a:AWSAccount)-[:RESOURCE]->(lambda:AWSLambda)
|
|
182
|
+
MATCH (lambda)-[:STS_ASSUMEROLE_ALLOW]->(role:AWSRole)
|
|
183
|
+
MATCH (role)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
184
|
+
WHERE stmt.effect = 'Allow'
|
|
185
|
+
WITH a, lambda, role, stmt,
|
|
186
|
+
// Define the list of IAM actions to match on
|
|
187
|
+
['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update'] AS patterns,
|
|
188
|
+
// Filter on the desired IAM actions
|
|
189
|
+
[action IN stmt.action
|
|
190
|
+
WHERE ANY(p IN ['iam:Create', 'iam:Attach', 'iam:Put', 'iam:Update', 'iam:Add'] WHERE action STARTS WITH p)
|
|
191
|
+
OR action = 'iam:*'
|
|
192
|
+
OR action = '*'
|
|
193
|
+
] AS actions
|
|
194
|
+
// Return only statement actions that we matched on
|
|
195
|
+
WHERE size(actions) > 0
|
|
196
|
+
UNWIND actions AS flat_action
|
|
197
|
+
WITH a, lambda, role, stmt,
|
|
198
|
+
collect(DISTINCT flat_action) AS actions
|
|
199
|
+
RETURN DISTINCT a.name AS account,
|
|
200
|
+
a.id as account_id,
|
|
201
|
+
lambda.arn AS arn,
|
|
202
|
+
lambda.description AS description,
|
|
203
|
+
lambda.anonymous_access AS internet_accessible,
|
|
204
|
+
role.name AS role_name,
|
|
205
|
+
actions
|
|
206
|
+
ORDER BY account, arn, internet_accessible
|
|
207
|
+
""",
|
|
208
|
+
cypher_visual_query="""
|
|
209
|
+
MATCH p = (a:AWSAccount)-[:RESOURCE]->(lambda:AWSLambda)
|
|
210
|
+
MATCH p1 = (lambda)-[:STS_ASSUMEROLE_ALLOW]->(role:AWSRole)
|
|
211
|
+
MATCH p2 = (role)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
212
|
+
WHERE stmt.effect = 'Allow'
|
|
213
|
+
AND ANY(action IN stmt.action WHERE
|
|
214
|
+
action STARTS WITH 'iam:Create'
|
|
215
|
+
OR action STARTS WITH 'iam:Attach'
|
|
216
|
+
OR action STARTS WITH 'iam:Put'
|
|
217
|
+
OR action STARTS WITH 'iam:Update'
|
|
218
|
+
OR action STARTS WITH 'iam:Add'
|
|
219
|
+
OR action = 'iam:*'
|
|
220
|
+
OR action = '*'
|
|
221
|
+
)
|
|
222
|
+
RETURN *
|
|
223
|
+
""",
|
|
224
|
+
module=Module.AWS,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
_aws_policy_manipulation_capabilities = Fact(
|
|
228
|
+
id="aws_policy_manipulation_capabilities",
|
|
229
|
+
name="Principals with IAM Policy Creation and Modification Capabilities",
|
|
230
|
+
description=(
|
|
231
|
+
"AWS IAM principals that can create, modify, or attach IAM policies to other principals. "
|
|
232
|
+
),
|
|
233
|
+
cypher_query="""
|
|
234
|
+
MATCH (a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
235
|
+
MATCH (principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
236
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
237
|
+
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
238
|
+
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
239
|
+
AND stmt.effect = 'Allow'
|
|
240
|
+
|
|
241
|
+
WITH a, principal, stmt,
|
|
242
|
+
// Return labels that are not the general "AWSPrincipal" label
|
|
243
|
+
[label IN labels(principal) WHERE label <> 'AWSPrincipal'][0] AS principal_type,
|
|
244
|
+
// Define the list of IAM actions to match on
|
|
245
|
+
[p IN
|
|
246
|
+
['iam:CreatePolicy', 'iam:CreatePolicyVersion', 'iam:AttachUserPolicy', 'iam:AttachRolePolicy', 'iam:AttachGroupPolicy',
|
|
247
|
+
'iam:AttachRolePolicy', 'iam:AttachGroupPolicy', 'iam:DetachUserPolicy', 'iam:DetachRolePolicy', 'iam:DetachGroupPolicy',
|
|
248
|
+
'iam:PutUserPolicy', 'iam:PutRolePolicy', 'iam:PutGroupPolicy'] |
|
|
249
|
+
p] AS patterns
|
|
250
|
+
|
|
251
|
+
// Return only statement actions that we matched on
|
|
252
|
+
WITH a, principal, principal_type, stmt,
|
|
253
|
+
[action IN stmt.action
|
|
254
|
+
WHERE ANY(p IN patterns WHERE action = p)
|
|
255
|
+
OR action = 'iam:*' OR action = '*'
|
|
256
|
+
] AS matched_actions
|
|
257
|
+
WHERE size(matched_actions) > 0
|
|
258
|
+
UNWIND matched_actions AS action
|
|
259
|
+
RETURN DISTINCT a.name AS account,
|
|
260
|
+
principal.name AS principal_name,
|
|
261
|
+
principal.arn AS principal_arn,
|
|
262
|
+
principal_type,
|
|
263
|
+
collect(action) as action,
|
|
264
|
+
stmt.resource AS resource
|
|
265
|
+
ORDER BY account, principal_name
|
|
266
|
+
""",
|
|
267
|
+
cypher_visual_query="""
|
|
268
|
+
MATCH p1=(a:AWSAccount)-[:RESOURCE]->(principal:AWSPrincipal)
|
|
269
|
+
MATCH p2=(principal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement)
|
|
270
|
+
WHERE NOT principal.name STARTS WITH 'AWSServiceRole'
|
|
271
|
+
AND NOT principal.name CONTAINS 'QuickSetup'
|
|
272
|
+
AND principal.name <> 'OrganizationAccountAccessRole'
|
|
273
|
+
AND stmt.effect = 'Allow'
|
|
274
|
+
AND ANY(action IN stmt.action WHERE
|
|
275
|
+
action CONTAINS 'iam:CreatePolicy' OR action CONTAINS 'iam:CreatePolicyVersion'
|
|
276
|
+
OR action CONTAINS 'iam:AttachUserPolicy' OR action CONTAINS 'iam:AttachRolePolicy'
|
|
277
|
+
OR action CONTAINS 'iam:AttachGroupPolicy' OR action CONTAINS 'iam:DetachUserPolicy'
|
|
278
|
+
OR action CONTAINS 'iam:DetachRolePolicy' OR action CONTAINS 'iam:DetachGroupPolicy'
|
|
279
|
+
OR action CONTAINS 'iam:PutUserPolicy' OR action CONTAINS 'iam:PutRolePolicy'
|
|
280
|
+
OR action CONTAINS 'iam:PutGroupPolicy' OR action = 'iam:*' OR action = '*'
|
|
281
|
+
)
|
|
282
|
+
RETURN *
|
|
283
|
+
""",
|
|
284
|
+
module=Module.AWS,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
t1098 = Requirement(
|
|
289
|
+
id="t1098",
|
|
290
|
+
name="Account Manipulation",
|
|
291
|
+
description=(
|
|
292
|
+
"Adversaries may manipulate accounts to maintain or elevate access to victim systems. "
|
|
293
|
+
"Activity that subverts security policies. For example in cloud this is "
|
|
294
|
+
"updating IAM policies or adding new global admins."
|
|
295
|
+
),
|
|
296
|
+
target_assets="Identities that can manipulate other identities",
|
|
297
|
+
facts=(
|
|
298
|
+
# AWS
|
|
299
|
+
_aws_account_manipulation_permissions,
|
|
300
|
+
_aws_trust_relationship_manipulation,
|
|
301
|
+
_aws_service_account_manipulation_via_ec2,
|
|
302
|
+
_aws_service_account_manipulation_via_lambda,
|
|
303
|
+
_aws_policy_manipulation_capabilities,
|
|
304
|
+
),
|
|
305
|
+
requirement_url="https://attack.mitre.org/techniques/T1098/",
|
|
306
|
+
attributes={
|
|
307
|
+
"tactic": "persistence,privilege_escalation",
|
|
308
|
+
"technique_id": "T1098",
|
|
309
|
+
"services": [
|
|
310
|
+
"iam",
|
|
311
|
+
"sts",
|
|
312
|
+
"ec2",
|
|
313
|
+
"lambda",
|
|
314
|
+
],
|
|
315
|
+
"providers": ["AWS"],
|
|
316
|
+
},
|
|
317
|
+
)
|
|
@@ -116,6 +116,7 @@ t1190 = Requirement(
|
|
|
116
116
|
id="t1190",
|
|
117
117
|
name="Exploit Public-Facing Application",
|
|
118
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
|
+
target_assets="Compute and storage resources that are accessible from the public internet",
|
|
119
120
|
facts=(
|
|
120
121
|
# AWS
|
|
121
122
|
_aws_ec2_instance_internet_exposed,
|
cartography/rules/spec/model.py
CHANGED
|
@@ -58,9 +58,21 @@ class Requirement:
|
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
id: str
|
|
61
|
+
"""A unique identifier for the requirement, e.g. T1098 in MITRE ATT&CK."""
|
|
61
62
|
name: str
|
|
63
|
+
"""A brief name for the requirement, e.g. "Account Manipulation"."""
|
|
62
64
|
description: str
|
|
65
|
+
"""A brief description of the requirement."""
|
|
66
|
+
target_assets: str
|
|
67
|
+
"""
|
|
68
|
+
A short description of the assets that this requirement is related to. E.g. "Cloud
|
|
69
|
+
identities that can manipulate other identities". This field is used as
|
|
70
|
+
documentation: `description` tells us information about the requirement;
|
|
71
|
+
`target_assets` tells us what specific objects in cartography we will search for to
|
|
72
|
+
find Facts related to the requirement.
|
|
73
|
+
"""
|
|
63
74
|
facts: tuple[Fact, ...]
|
|
75
|
+
"""The facts that are related to this requirement."""
|
|
64
76
|
attributes: dict[str, Any] | None = None
|
|
65
77
|
"""
|
|
66
78
|
Metadata attributes for the requirement. Example:
|
|
@@ -74,6 +86,7 @@ class Requirement:
|
|
|
74
86
|
```
|
|
75
87
|
"""
|
|
76
88
|
requirement_url: str | None = None
|
|
89
|
+
"""A URL reference to the requirement in the framework, e.g. https://attack.mitre.org/techniques/T1098/"""
|
|
77
90
|
|
|
78
91
|
|
|
79
92
|
@dataclass(frozen=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.117.0
|
|
4
4
|
Summary: Explore assets and their relationships across your technical infrastructure.
|
|
5
5
|
Maintainer: Cartography Contributors
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -42,6 +42,7 @@ Requires-Dist: python-digitalocean>=1.16.0
|
|
|
42
42
|
Requires-Dist: adal>=1.2.4
|
|
43
43
|
Requires-Dist: azure-cli-core>=2.26.0
|
|
44
44
|
Requires-Dist: azure-mgmt-compute>=5.0.0
|
|
45
|
+
Requires-Dist: azure-mgmt-containerinstance>=10.0.0
|
|
45
46
|
Requires-Dist: azure-mgmt-resource>=10.2.0
|
|
46
47
|
Requires-Dist: azure-mgmt-cosmosdb>=6.0.0
|
|
47
48
|
Requires-Dist: azure-mgmt-web>=7.0.0
|
|
@@ -102,7 +103,7 @@ You can learn more about the story behind Cartography in our [presentation at BS
|
|
|
102
103
|
- [Keycloak](https://cartography-cncf.github.io/cartography/modules/keycloak/index.html) - Realms, Users, Groups, Roles, Scopes, Clients, IdentityProviders, Authentication Flows, Authentication Executions, Organizations, Organization Domains
|
|
103
104
|
- [Kubernetes](https://cartography-cncf.github.io/cartography/modules/kubernetes/index.html) - Cluster, Namespace, Service, Pod, Container, ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding, OIDCProvider
|
|
104
105
|
- [Lastpass](https://cartography-cncf.github.io/cartography/modules/lastpass/index.html) - users
|
|
105
|
-
- [Microsoft Azure](https://cartography-cncf.github.io/cartography/modules/azure/index.html) - App Service, CosmosDB, Functions, Logic Apps, Resource Group, SQL, Storage, Virtual Machine
|
|
106
|
+
- [Microsoft Azure](https://cartography-cncf.github.io/cartography/modules/azure/index.html) - App Service, Container Instance, CosmosDB, Functions, Logic Apps, Resource Group, SQL, Storage, Virtual Machine
|
|
106
107
|
- [Microsoft Entra ID](https://cartography-cncf.github.io/cartography/modules/entra/index.html) - Users, Groups, Applications, OUs, App Roles, federation to AWS Identity Center
|
|
107
108
|
- [NIST CVE](https://cartography-cncf.github.io/cartography/modules/cve/index.html) - Common Vulnerabilities and Exposures (CVE) data from NIST database
|
|
108
109
|
- [Okta](https://cartography-cncf.github.io/cartography/modules/okta/index.html) - users, groups, organizations, roles, applications, factors, trusted origins, reply URIs, federation to AWS roles, federation to AWS Identity Center
|