cartography 0.95.0rc1__py3-none-any.whl → 0.96.0rc1__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/graph/querybuilder.py +4 -0
- cartography/intel/aws/ec2/network_acls.py +208 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/semgrep/__init__.py +9 -2
- cartography/intel/semgrep/dependencies.py +201 -0
- cartography/intel/semgrep/deployment.py +67 -0
- cartography/intel/semgrep/findings.py +22 -53
- cartography/models/aws/ec2/network_acl_rules.py +97 -0
- cartography/models/aws/ec2/network_acls.py +86 -0
- cartography/models/core/common.py +18 -1
- cartography/models/semgrep/dependencies.py +77 -0
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/METADATA +1 -1
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/RECORD +17 -11
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/WHEEL +1 -1
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/LICENSE +0 -0
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/entry_points.txt +0 -0
- {cartography-0.95.0rc1.dist-info → cartography-0.96.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -118,6 +118,7 @@ def _build_where_clause_for_rel_match(node_var: str, matcher: TargetNodeMatcher)
|
|
|
118
118
|
"""
|
|
119
119
|
match = Template("$node_var.$key = $prop_ref")
|
|
120
120
|
case_insensitive_match = Template("toLower($node_var.$key) = toLower($prop_ref)")
|
|
121
|
+
fuzzy_and_ignorecase_match = Template("toLower($node_var.$key) CONTAINS toLower($prop_ref)")
|
|
121
122
|
|
|
122
123
|
matcher_asdict = asdict(matcher)
|
|
123
124
|
|
|
@@ -125,7 +126,10 @@ def _build_where_clause_for_rel_match(node_var: str, matcher: TargetNodeMatcher)
|
|
|
125
126
|
for key, prop_ref in matcher_asdict.items():
|
|
126
127
|
if prop_ref.ignore_case:
|
|
127
128
|
prop_line = case_insensitive_match.safe_substitute(node_var=node_var, key=key, prop_ref=prop_ref)
|
|
129
|
+
elif prop_ref.fuzzy_and_ignore_case:
|
|
130
|
+
prop_line = fuzzy_and_ignorecase_match.safe_substitute(node_var=node_var, key=key, prop_ref=prop_ref)
|
|
128
131
|
else:
|
|
132
|
+
# Exact match (default; most efficient)
|
|
129
133
|
prop_line = match.safe_substitute(node_var=node_var, key=key, prop_ref=prop_ref)
|
|
130
134
|
result.append(prop_line)
|
|
131
135
|
return ' AND\n'.join(result)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
import neo4j
|
|
7
|
+
|
|
8
|
+
from .util import get_botocore_config
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.models.aws.ec2.network_acl_rules import EC2NetworkAclEgressRuleSchema
|
|
12
|
+
from cartography.models.aws.ec2.network_acl_rules import EC2NetworkAclInboundRuleSchema
|
|
13
|
+
from cartography.models.aws.ec2.network_acls import EC2NetworkAclSchema
|
|
14
|
+
from cartography.util import aws_handle_regions
|
|
15
|
+
from cartography.util import timeit
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
Ec2AclObjects = namedtuple(
|
|
20
|
+
"Ec2AclObjects", [
|
|
21
|
+
'network_acls',
|
|
22
|
+
'inbound_rules',
|
|
23
|
+
'outbound_rules',
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@timeit
|
|
29
|
+
@aws_handle_regions
|
|
30
|
+
def get_network_acl_data(boto3_session: boto3.session.Session, region: str) -> list[dict[str, Any]]:
|
|
31
|
+
client = boto3_session.client('ec2', region_name=region, config=get_botocore_config())
|
|
32
|
+
paginator = client.get_paginator('describe_network_acls')
|
|
33
|
+
acls = []
|
|
34
|
+
for page in paginator.paginate():
|
|
35
|
+
acls.extend(page['NetworkAcls'])
|
|
36
|
+
return acls
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def transform_network_acl_data(
|
|
40
|
+
data_list: list[dict[str, Any]],
|
|
41
|
+
region: str,
|
|
42
|
+
current_aws_account_id: str,
|
|
43
|
+
) -> Ec2AclObjects:
|
|
44
|
+
network_acls = []
|
|
45
|
+
inbound_rules = []
|
|
46
|
+
outbound_rules = []
|
|
47
|
+
|
|
48
|
+
for network_acl in data_list:
|
|
49
|
+
network_acl_id = network_acl['NetworkAclId']
|
|
50
|
+
base_network_acl = {
|
|
51
|
+
'Id': network_acl_id,
|
|
52
|
+
'Arn': f'arn:aws:ec2:{region}:{current_aws_account_id}:network-acl/{network_acl_id}',
|
|
53
|
+
'IsDefault': network_acl['IsDefault'],
|
|
54
|
+
'VpcId': network_acl['VpcId'],
|
|
55
|
+
'OwnerId': network_acl['OwnerId'],
|
|
56
|
+
}
|
|
57
|
+
if network_acl.get('Associations') and network_acl['Associations']:
|
|
58
|
+
# Include subnet associations in the data object if they exist
|
|
59
|
+
for association in network_acl['Associations']:
|
|
60
|
+
base_network_acl['NetworkAclAssociationId'] = association['NetworkAclAssociationId']
|
|
61
|
+
base_network_acl['SubnetId'] = association['SubnetId']
|
|
62
|
+
network_acls.append(base_network_acl)
|
|
63
|
+
else:
|
|
64
|
+
# Otherwise if there's no associations then don't include that in the data object
|
|
65
|
+
network_acls.append(base_network_acl)
|
|
66
|
+
|
|
67
|
+
if network_acl.get("Entries"):
|
|
68
|
+
for rule in network_acl["Entries"]:
|
|
69
|
+
direction = 'egress' if rule['Egress'] else 'inbound'
|
|
70
|
+
transformed_rule = {
|
|
71
|
+
'Id': f"{network_acl['NetworkAclId']}/{direction}/{rule['RuleNumber']}",
|
|
72
|
+
'CidrBlock': rule['CidrBlock'],
|
|
73
|
+
'Egress': rule['Egress'],
|
|
74
|
+
'Protocol': rule['Protocol'],
|
|
75
|
+
'RuleAction': rule['RuleAction'],
|
|
76
|
+
'RuleNumber': rule['RuleNumber'],
|
|
77
|
+
# Add pointer back to the nacl to create an edge
|
|
78
|
+
'NetworkAclId': network_acl_id,
|
|
79
|
+
'FromPort': rule.get('PortRange', {}).get('FromPort'),
|
|
80
|
+
'ToPort': rule.get('PortRange', {}).get('ToPort'),
|
|
81
|
+
}
|
|
82
|
+
if transformed_rule['Egress']:
|
|
83
|
+
outbound_rules.append(transformed_rule)
|
|
84
|
+
else:
|
|
85
|
+
inbound_rules.append(transformed_rule)
|
|
86
|
+
return Ec2AclObjects(
|
|
87
|
+
network_acls=network_acls,
|
|
88
|
+
inbound_rules=inbound_rules,
|
|
89
|
+
outbound_rules=outbound_rules,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@timeit
|
|
94
|
+
def load_all_nacl_data(
|
|
95
|
+
neo4j_session: neo4j.Session,
|
|
96
|
+
ec2_acl_objects: Ec2AclObjects,
|
|
97
|
+
region: str,
|
|
98
|
+
aws_account_id: str,
|
|
99
|
+
update_tag: int,
|
|
100
|
+
) -> None:
|
|
101
|
+
load_network_acls(
|
|
102
|
+
neo4j_session,
|
|
103
|
+
ec2_acl_objects.network_acls,
|
|
104
|
+
region,
|
|
105
|
+
aws_account_id,
|
|
106
|
+
update_tag,
|
|
107
|
+
)
|
|
108
|
+
load_network_acl_inbound_rules(
|
|
109
|
+
neo4j_session,
|
|
110
|
+
ec2_acl_objects.inbound_rules,
|
|
111
|
+
region,
|
|
112
|
+
aws_account_id,
|
|
113
|
+
update_tag,
|
|
114
|
+
)
|
|
115
|
+
load_network_acl_egress_rules(
|
|
116
|
+
neo4j_session,
|
|
117
|
+
ec2_acl_objects.outbound_rules,
|
|
118
|
+
region,
|
|
119
|
+
aws_account_id,
|
|
120
|
+
update_tag,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@timeit
|
|
125
|
+
def load_network_acls(
|
|
126
|
+
neo4j_session: neo4j.Session,
|
|
127
|
+
data: list[dict[str, Any]],
|
|
128
|
+
region: str,
|
|
129
|
+
aws_account_id: str,
|
|
130
|
+
update_tag: int,
|
|
131
|
+
) -> None:
|
|
132
|
+
logger.info(f"Loading {len(data)} network acls in {region}.")
|
|
133
|
+
load(
|
|
134
|
+
neo4j_session,
|
|
135
|
+
EC2NetworkAclSchema(),
|
|
136
|
+
data,
|
|
137
|
+
Region=region,
|
|
138
|
+
AWS_ID=aws_account_id,
|
|
139
|
+
lastupdated=update_tag,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@timeit
|
|
144
|
+
def load_network_acl_inbound_rules(
|
|
145
|
+
neo4j_session: neo4j.Session,
|
|
146
|
+
data: list[dict[str, Any]],
|
|
147
|
+
region: str,
|
|
148
|
+
aws_account_id: str,
|
|
149
|
+
update_tag: int,
|
|
150
|
+
) -> None:
|
|
151
|
+
logger.info(f"Loading {len(data)} network acl inbound rules in {region}.")
|
|
152
|
+
load(
|
|
153
|
+
neo4j_session,
|
|
154
|
+
EC2NetworkAclInboundRuleSchema(),
|
|
155
|
+
data,
|
|
156
|
+
Region=region,
|
|
157
|
+
AWS_ID=aws_account_id,
|
|
158
|
+
lastupdated=update_tag,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@timeit
|
|
163
|
+
def load_network_acl_egress_rules(
|
|
164
|
+
neo4j_session: neo4j.Session,
|
|
165
|
+
data: list[dict[str, Any]],
|
|
166
|
+
region: str,
|
|
167
|
+
aws_account_id: str,
|
|
168
|
+
update_tag: int,
|
|
169
|
+
) -> None:
|
|
170
|
+
logger.info(f"Loading {len(data)} network acl egress rules in {region}.")
|
|
171
|
+
load(
|
|
172
|
+
neo4j_session,
|
|
173
|
+
EC2NetworkAclEgressRuleSchema(),
|
|
174
|
+
data,
|
|
175
|
+
Region=region,
|
|
176
|
+
AWS_ID=aws_account_id,
|
|
177
|
+
lastupdated=update_tag,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@timeit
|
|
182
|
+
def cleanup_network_acls(neo4j_session: neo4j.Session, common_job_parameters: dict[str, Any]) -> None:
|
|
183
|
+
GraphJob.from_node_schema(EC2NetworkAclSchema(), common_job_parameters).run(neo4j_session)
|
|
184
|
+
GraphJob.from_node_schema(EC2NetworkAclInboundRuleSchema(), common_job_parameters).run(neo4j_session)
|
|
185
|
+
GraphJob.from_node_schema(EC2NetworkAclEgressRuleSchema(), common_job_parameters).run(neo4j_session)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@timeit
|
|
189
|
+
def sync_network_acls(
|
|
190
|
+
neo4j_session: neo4j.Session,
|
|
191
|
+
boto3_session: boto3.session.Session,
|
|
192
|
+
regions: list[str],
|
|
193
|
+
current_aws_account_id: str,
|
|
194
|
+
update_tag: int,
|
|
195
|
+
common_job_parameters: dict[str, Any],
|
|
196
|
+
) -> None:
|
|
197
|
+
for region in regions:
|
|
198
|
+
logger.info(f"Syncing EC2 network ACLs for region '{region}' in account '{current_aws_account_id}'.")
|
|
199
|
+
data = get_network_acl_data(boto3_session, region)
|
|
200
|
+
ec2_acl_data = transform_network_acl_data(data, region, current_aws_account_id)
|
|
201
|
+
load_all_nacl_data(
|
|
202
|
+
neo4j_session,
|
|
203
|
+
ec2_acl_data,
|
|
204
|
+
region,
|
|
205
|
+
current_aws_account_id,
|
|
206
|
+
update_tag,
|
|
207
|
+
)
|
|
208
|
+
cleanup_network_acls(neo4j_session, common_job_parameters)
|
|
@@ -32,6 +32,7 @@ from .ec2.key_pairs import sync_ec2_key_pairs
|
|
|
32
32
|
from .ec2.launch_templates import sync_ec2_launch_templates
|
|
33
33
|
from .ec2.load_balancer_v2s import sync_load_balancer_v2s
|
|
34
34
|
from .ec2.load_balancers import sync_load_balancers
|
|
35
|
+
from .ec2.network_acls import sync_network_acls
|
|
35
36
|
from .ec2.network_interfaces import sync_network_interfaces
|
|
36
37
|
from .ec2.reserved_instances import sync_ec2_reserved_instances
|
|
37
38
|
from .ec2.security_groups import sync_ec2_security_groupinfo
|
|
@@ -55,6 +56,7 @@ RESOURCE_FUNCTIONS: Dict = {
|
|
|
55
56
|
'ec2:keypair': sync_ec2_key_pairs,
|
|
56
57
|
'ec2:load_balancer': sync_load_balancers,
|
|
57
58
|
'ec2:load_balancer_v2': sync_load_balancer_v2s,
|
|
59
|
+
'ec2:network_acls': sync_network_acls,
|
|
58
60
|
'ec2:network_interface': sync_network_interfaces,
|
|
59
61
|
'ec2:security_group': sync_ec2_security_groupinfo,
|
|
60
62
|
'ec2:subnet': sync_subnets,
|
|
@@ -3,7 +3,9 @@ import logging
|
|
|
3
3
|
import neo4j
|
|
4
4
|
|
|
5
5
|
from cartography.config import Config
|
|
6
|
-
from cartography.intel.semgrep.
|
|
6
|
+
from cartography.intel.semgrep.dependencies import sync_dependencies
|
|
7
|
+
from cartography.intel.semgrep.deployment import sync_deployment
|
|
8
|
+
from cartography.intel.semgrep.findings import sync_findings
|
|
7
9
|
from cartography.util import timeit
|
|
8
10
|
|
|
9
11
|
|
|
@@ -20,4 +22,9 @@ def start_semgrep_ingestion(
|
|
|
20
22
|
if not config.semgrep_app_token:
|
|
21
23
|
logger.info('Semgrep import is not configured - skipping this module. See docs to configure.')
|
|
22
24
|
return
|
|
23
|
-
|
|
25
|
+
|
|
26
|
+
# sync_deployment must be called first since it populates common_job_parameters
|
|
27
|
+
# with the deployment ID and slug, which are required by the other sync functions
|
|
28
|
+
sync_deployment(neo4j_session, config.semgrep_app_token, config.update_tag, common_job_parameters)
|
|
29
|
+
sync_dependencies(neo4j_session, config.semgrep_app_token, config.update_tag, common_job_parameters)
|
|
30
|
+
sync_findings(neo4j_session, config.semgrep_app_token, config.update_tag, common_job_parameters)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import neo4j
|
|
8
|
+
import requests
|
|
9
|
+
from requests.exceptions import HTTPError
|
|
10
|
+
from requests.exceptions import ReadTimeout
|
|
11
|
+
|
|
12
|
+
from cartography.client.core.tx import load
|
|
13
|
+
from cartography.graph.job import GraphJob
|
|
14
|
+
from cartography.models.semgrep.dependencies import SemgrepGoLibrarySchema
|
|
15
|
+
from cartography.stats import get_stats_client
|
|
16
|
+
from cartography.util import merge_module_sync_metadata
|
|
17
|
+
from cartography.util import timeit
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
stat_handler = get_stats_client(__name__)
|
|
21
|
+
_PAGE_SIZE = 10000
|
|
22
|
+
_TIMEOUT = (60, 60)
|
|
23
|
+
_MAX_RETRIES = 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@timeit
|
|
27
|
+
def get_dependencies(semgrep_app_token: str, deployment_id: str, ecosystems: List[str]) -> List[Dict[str, Any]]:
|
|
28
|
+
"""
|
|
29
|
+
Gets all dependencies for the given ecosystems within the given Semgrep deployment ID.
|
|
30
|
+
param: semgrep_app_token: The Semgrep App token to use for authentication.
|
|
31
|
+
param: deployment_id: The Semgrep deployment ID to use for retrieving dependencies.
|
|
32
|
+
param: ecosystems: One or more ecosystems to import dependencies from, e.g. "gomod" or "pypi".
|
|
33
|
+
The list of supported ecosystems is defined here:
|
|
34
|
+
https://semgrep.dev/api/v1/docs/#tag/SupplyChainService/operation/semgrep_app.products.sca.handlers.dependency.list_dependencies_conexxion
|
|
35
|
+
"""
|
|
36
|
+
all_deps = []
|
|
37
|
+
deps_url = f"https://semgrep.dev/api/v1/deployments/{deployment_id}/dependencies"
|
|
38
|
+
has_more = True
|
|
39
|
+
page = 0
|
|
40
|
+
retries = 0
|
|
41
|
+
headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Authorization": f"Bearer {semgrep_app_token}",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
request_data: dict[str, Any] = {
|
|
47
|
+
"pageSize": _PAGE_SIZE,
|
|
48
|
+
"dependencyFilter": {
|
|
49
|
+
"ecosystem": ecosystems,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
logger.info(f"Retrieving Semgrep dependencies for deployment '{deployment_id}'.")
|
|
54
|
+
while has_more:
|
|
55
|
+
try:
|
|
56
|
+
response = requests.post(deps_url, json=request_data, headers=headers, timeout=_TIMEOUT)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
data = response.json()
|
|
59
|
+
except (ReadTimeout, HTTPError):
|
|
60
|
+
logger.warning(f"Failed to retrieve Semgrep dependencies for page {page}. Retrying...")
|
|
61
|
+
retries += 1
|
|
62
|
+
if retries >= _MAX_RETRIES:
|
|
63
|
+
raise
|
|
64
|
+
continue
|
|
65
|
+
deps = data.get("dependencies", [])
|
|
66
|
+
has_more = data.get("hasMore", False)
|
|
67
|
+
logger.info(f"Processed page {page} of Semgrep dependencies.")
|
|
68
|
+
all_deps.extend(deps)
|
|
69
|
+
retries = 0
|
|
70
|
+
page += 1
|
|
71
|
+
request_data["cursor"] = data.get("cursor")
|
|
72
|
+
|
|
73
|
+
logger.info(f"Retrieved {len(all_deps)} Semgrep dependencies in {page} pages.")
|
|
74
|
+
return all_deps
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def transform_dependencies(raw_deps: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
78
|
+
"""
|
|
79
|
+
Transforms the raw dependencies response from Semgrep API into a list of dicts
|
|
80
|
+
that can be used to create the Dependency nodes.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
sample raw_dep as of November 2024:
|
|
85
|
+
{
|
|
86
|
+
"repositoryId": "123456",
|
|
87
|
+
"definedAt": {
|
|
88
|
+
"path": "go.mod",
|
|
89
|
+
"startLine": "6",
|
|
90
|
+
"endLine": "6",
|
|
91
|
+
"url": "https://github.com/org/repo-name/blob/00000000000000000000000000000000/go.mod#L6",
|
|
92
|
+
"committedAt": "1970-01-01T00:00:00Z",
|
|
93
|
+
"startCol": "0",
|
|
94
|
+
"endCol": "0"
|
|
95
|
+
},
|
|
96
|
+
"transitivity": "DIRECT",
|
|
97
|
+
"package": {
|
|
98
|
+
"name": "github.com/foo/bar",
|
|
99
|
+
"versionSpecifier": "1.2.3"
|
|
100
|
+
},
|
|
101
|
+
"ecosystem": "gomod",
|
|
102
|
+
"licenses": [],
|
|
103
|
+
"pathToTransitivity": []
|
|
104
|
+
},
|
|
105
|
+
"""
|
|
106
|
+
deps = []
|
|
107
|
+
for raw_dep in raw_deps:
|
|
108
|
+
|
|
109
|
+
# We could call a different endpoint to get all repo IDs and store a mapping of repo ID to URL,
|
|
110
|
+
# but it's much simpler to just extract the URL from the definedAt field.
|
|
111
|
+
repo_url = raw_dep["definedAt"]["url"].split("/blob/", 1)[0]
|
|
112
|
+
|
|
113
|
+
name = raw_dep["package"]["name"]
|
|
114
|
+
version = raw_dep["package"]["versionSpecifier"]
|
|
115
|
+
id = f"{name}|{version}"
|
|
116
|
+
|
|
117
|
+
# As of November 2024, Semgrep does not import dependencies with version specifiers such as >, <, etc.
|
|
118
|
+
# For now, hardcode the specifier to ==<version> to align with GitHub-sourced Python dependencies.
|
|
119
|
+
# If Semgrep eventually supports version specifiers, update this line accordingly.
|
|
120
|
+
specifier = f"=={version}"
|
|
121
|
+
|
|
122
|
+
deps.append({
|
|
123
|
+
# existing dependency properties:
|
|
124
|
+
"id": id,
|
|
125
|
+
"name": name,
|
|
126
|
+
"specifier": specifier,
|
|
127
|
+
"version": version,
|
|
128
|
+
"repo_url": repo_url,
|
|
129
|
+
|
|
130
|
+
# Semgrep-specific properties:
|
|
131
|
+
"ecosystem": raw_dep["ecosystem"],
|
|
132
|
+
"transitivity": raw_dep["transitivity"].lower(),
|
|
133
|
+
"url": raw_dep["definedAt"]["url"],
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return deps
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@timeit
|
|
140
|
+
def load_dependencies(
|
|
141
|
+
neo4j_session: neo4j.Session,
|
|
142
|
+
dependency_schema: Callable,
|
|
143
|
+
dependencies: List[Dict],
|
|
144
|
+
deployment_id: str,
|
|
145
|
+
update_tag: int,
|
|
146
|
+
) -> None:
|
|
147
|
+
logger.info(f"Loading {len(dependencies)} {dependency_schema().label} objects into the graph.")
|
|
148
|
+
load(
|
|
149
|
+
neo4j_session,
|
|
150
|
+
dependency_schema(),
|
|
151
|
+
dependencies,
|
|
152
|
+
lastupdated=update_tag,
|
|
153
|
+
DEPLOYMENT_ID=deployment_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@timeit
|
|
158
|
+
def cleanup(
|
|
159
|
+
neo4j_session: neo4j.Session,
|
|
160
|
+
common_job_parameters: Dict[str, Any],
|
|
161
|
+
) -> None:
|
|
162
|
+
logger.info("Running Semgrep Go Library cleanup job.")
|
|
163
|
+
go_libraries_cleanup_job = GraphJob.from_node_schema(
|
|
164
|
+
SemgrepGoLibrarySchema(), common_job_parameters,
|
|
165
|
+
)
|
|
166
|
+
go_libraries_cleanup_job.run(neo4j_session)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@timeit
|
|
170
|
+
def sync_dependencies(
|
|
171
|
+
neo4j_session: neo4j.Session,
|
|
172
|
+
semgrep_app_token: str,
|
|
173
|
+
update_tag: int,
|
|
174
|
+
common_job_parameters: Dict[str, Any],
|
|
175
|
+
) -> None:
|
|
176
|
+
|
|
177
|
+
deployment_id = common_job_parameters.get("DEPLOYMENT_ID")
|
|
178
|
+
if not deployment_id:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"Missing Semgrep deployment ID, ensure that sync_deployment() has been called."
|
|
181
|
+
"Skipping Semgrep dependencies sync job.",
|
|
182
|
+
)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
logger.info("Running Semgrep dependencies sync job.")
|
|
186
|
+
|
|
187
|
+
# fetch and load dependencies for the Go ecosystem
|
|
188
|
+
raw_go_deps = get_dependencies(semgrep_app_token, deployment_id, ecosystems=["gomod"])
|
|
189
|
+
go_deps = transform_dependencies(raw_go_deps)
|
|
190
|
+
load_dependencies(neo4j_session, SemgrepGoLibrarySchema, go_deps, deployment_id, update_tag)
|
|
191
|
+
|
|
192
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
193
|
+
|
|
194
|
+
merge_module_sync_metadata(
|
|
195
|
+
neo4j_session=neo4j_session,
|
|
196
|
+
group_type='Semgrep',
|
|
197
|
+
group_id=deployment_id,
|
|
198
|
+
synced_type='SemgrepDependency',
|
|
199
|
+
update_tag=update_tag,
|
|
200
|
+
stat_handler=stat_handler,
|
|
201
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
import neo4j
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from cartography.client.core.tx import load
|
|
9
|
+
from cartography.models.semgrep.deployment import SemgrepDeploymentSchema
|
|
10
|
+
from cartography.stats import get_stats_client
|
|
11
|
+
from cartography.util import timeit
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
stat_handler = get_stats_client(__name__)
|
|
15
|
+
_TIMEOUT = (60, 60)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@timeit
|
|
19
|
+
def get_deployment(semgrep_app_token: str) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Gets the deployment associated with the passed Semgrep App token.
|
|
22
|
+
param: semgrep_app_token: The Semgrep App token to use for authentication.
|
|
23
|
+
"""
|
|
24
|
+
deployment = {}
|
|
25
|
+
deployment_url = "https://semgrep.dev/api/v1/deployments"
|
|
26
|
+
headers = {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"Authorization": f"Bearer {semgrep_app_token}",
|
|
29
|
+
}
|
|
30
|
+
response = requests.get(deployment_url, headers=headers, timeout=_TIMEOUT)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
|
|
33
|
+
data = response.json()
|
|
34
|
+
deployment["id"] = data["deployments"][0]["id"]
|
|
35
|
+
deployment["name"] = data["deployments"][0]["name"]
|
|
36
|
+
deployment["slug"] = data["deployments"][0]["slug"]
|
|
37
|
+
|
|
38
|
+
return deployment
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@timeit
|
|
42
|
+
def load_semgrep_deployment(
|
|
43
|
+
neo4j_session: neo4j.Session, deployment: Dict[str, Any], update_tag: int,
|
|
44
|
+
) -> None:
|
|
45
|
+
logger.info(f"Loading SemgrepDeployment {deployment} into the graph.")
|
|
46
|
+
load(
|
|
47
|
+
neo4j_session,
|
|
48
|
+
SemgrepDeploymentSchema(),
|
|
49
|
+
[deployment],
|
|
50
|
+
lastupdated=update_tag,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@timeit
|
|
55
|
+
def sync_deployment(
|
|
56
|
+
neo4j_session: neo4j.Session,
|
|
57
|
+
semgrep_app_token: str,
|
|
58
|
+
update_tag: int,
|
|
59
|
+
common_job_parameters: Dict[str, Any],
|
|
60
|
+
) -> None:
|
|
61
|
+
|
|
62
|
+
semgrep_deployment = get_deployment(semgrep_app_token)
|
|
63
|
+
deployment_id = semgrep_deployment["id"]
|
|
64
|
+
deployment_slug = semgrep_deployment["slug"]
|
|
65
|
+
load_semgrep_deployment(neo4j_session, semgrep_deployment, update_tag)
|
|
66
|
+
common_job_parameters["DEPLOYMENT_ID"] = deployment_id
|
|
67
|
+
common_job_parameters["DEPLOYMENT_SLUG"] = deployment_slug
|
|
@@ -11,7 +11,6 @@ from requests.exceptions import ReadTimeout
|
|
|
11
11
|
|
|
12
12
|
from cartography.client.core.tx import load
|
|
13
13
|
from cartography.graph.job import GraphJob
|
|
14
|
-
from cartography.models.semgrep.deployment import SemgrepDeploymentSchema
|
|
15
14
|
from cartography.models.semgrep.findings import SemgrepSCAFindingSchema
|
|
16
15
|
from cartography.models.semgrep.locations import SemgrepSCALocationSchema
|
|
17
16
|
from cartography.stats import get_stats_client
|
|
@@ -26,29 +25,6 @@ _TIMEOUT = (60, 60)
|
|
|
26
25
|
_MAX_RETRIES = 3
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
@timeit
|
|
30
|
-
def get_deployment(semgrep_app_token: str) -> Dict[str, Any]:
|
|
31
|
-
"""
|
|
32
|
-
Gets the deployment associated with the passed Semgrep App token.
|
|
33
|
-
param: semgrep_app_token: The Semgrep App token to use for authentication.
|
|
34
|
-
"""
|
|
35
|
-
deployment = {}
|
|
36
|
-
deployment_url = "https://semgrep.dev/api/v1/deployments"
|
|
37
|
-
headers = {
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
"Authorization": f"Bearer {semgrep_app_token}",
|
|
40
|
-
}
|
|
41
|
-
response = requests.get(deployment_url, headers=headers, timeout=_TIMEOUT)
|
|
42
|
-
response.raise_for_status()
|
|
43
|
-
|
|
44
|
-
data = response.json()
|
|
45
|
-
deployment["id"] = data["deployments"][0]["id"]
|
|
46
|
-
deployment["name"] = data["deployments"][0]["name"]
|
|
47
|
-
deployment["slug"] = data["deployments"][0]["slug"]
|
|
48
|
-
|
|
49
|
-
return deployment
|
|
50
|
-
|
|
51
|
-
|
|
52
28
|
@timeit
|
|
53
29
|
def get_sca_vulns(semgrep_app_token: str, deployment_slug: str) -> List[Dict[str, Any]]:
|
|
54
30
|
"""
|
|
@@ -81,11 +57,11 @@ def get_sca_vulns(semgrep_app_token: str, deployment_slug: str) -> List[Dict[str
|
|
|
81
57
|
response = requests.get(sca_url, params=request_data, headers=headers, timeout=_TIMEOUT)
|
|
82
58
|
response.raise_for_status()
|
|
83
59
|
data = response.json()
|
|
84
|
-
except (ReadTimeout, HTTPError)
|
|
60
|
+
except (ReadTimeout, HTTPError):
|
|
85
61
|
logger.warning(f"Failed to retrieve Semgrep SCA vulns for page {page}. Retrying...")
|
|
86
62
|
retries += 1
|
|
87
63
|
if retries >= _MAX_RETRIES:
|
|
88
|
-
raise
|
|
64
|
+
raise
|
|
89
65
|
continue
|
|
90
66
|
vulns = data["findings"]
|
|
91
67
|
has_more = len(vulns) > 0
|
|
@@ -201,19 +177,6 @@ def transform_sca_vulns(raw_vulns: List[Dict[str, Any]]) -> Tuple[List[Dict[str,
|
|
|
201
177
|
return vulns, usages
|
|
202
178
|
|
|
203
179
|
|
|
204
|
-
@timeit
|
|
205
|
-
def load_semgrep_deployment(
|
|
206
|
-
neo4j_session: neo4j.Session, deployment: Dict[str, Any], update_tag: int,
|
|
207
|
-
) -> None:
|
|
208
|
-
logger.info(f"Loading Semgrep deployment info {deployment} into the graph...")
|
|
209
|
-
load(
|
|
210
|
-
neo4j_session,
|
|
211
|
-
SemgrepDeploymentSchema(),
|
|
212
|
-
[deployment],
|
|
213
|
-
lastupdated=update_tag,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
180
|
@timeit
|
|
218
181
|
def load_semgrep_sca_vulns(
|
|
219
182
|
neo4j_session: neo4j.Session,
|
|
@@ -221,7 +184,7 @@ def load_semgrep_sca_vulns(
|
|
|
221
184
|
deployment_id: str,
|
|
222
185
|
update_tag: int,
|
|
223
186
|
) -> None:
|
|
224
|
-
logger.info(f"Loading {len(vulns)}
|
|
187
|
+
logger.info(f"Loading {len(vulns)} SemgrepSCAFinding objects into the graph.")
|
|
225
188
|
load(
|
|
226
189
|
neo4j_session,
|
|
227
190
|
SemgrepSCAFindingSchema(),
|
|
@@ -238,7 +201,7 @@ def load_semgrep_sca_usages(
|
|
|
238
201
|
deployment_id: str,
|
|
239
202
|
update_tag: int,
|
|
240
203
|
) -> None:
|
|
241
|
-
logger.info(f"Loading {len(usages)}
|
|
204
|
+
logger.info(f"Loading {len(usages)} SemgrepSCALocation objects into the graph.")
|
|
242
205
|
load(
|
|
243
206
|
neo4j_session,
|
|
244
207
|
SemgrepSCALocationSchema(),
|
|
@@ -265,26 +228,32 @@ def cleanup(
|
|
|
265
228
|
|
|
266
229
|
|
|
267
230
|
@timeit
|
|
268
|
-
def
|
|
269
|
-
|
|
231
|
+
def sync_findings(
|
|
232
|
+
neo4j_session: neo4j.Session,
|
|
270
233
|
semgrep_app_token: str,
|
|
271
234
|
update_tag: int,
|
|
272
235
|
common_job_parameters: Dict[str, Any],
|
|
273
236
|
) -> None:
|
|
237
|
+
|
|
238
|
+
deployment_id = common_job_parameters.get("DEPLOYMENT_ID")
|
|
239
|
+
deployment_slug = common_job_parameters.get("DEPLOYMENT_SLUG")
|
|
240
|
+
if not deployment_id or not deployment_slug:
|
|
241
|
+
logger.warning(
|
|
242
|
+
"Missing Semgrep deployment ID or slug, ensure that sync_deployment() has been called."
|
|
243
|
+
"Skipping SCA findings sync job.",
|
|
244
|
+
)
|
|
245
|
+
return
|
|
246
|
+
|
|
274
247
|
logger.info("Running Semgrep SCA findings sync job.")
|
|
275
|
-
semgrep_deployment = get_deployment(semgrep_app_token)
|
|
276
|
-
deployment_id = semgrep_deployment["id"]
|
|
277
|
-
deployment_slug = semgrep_deployment["slug"]
|
|
278
|
-
load_semgrep_deployment(neo4j_sesion, semgrep_deployment, update_tag)
|
|
279
|
-
common_job_parameters["DEPLOYMENT_ID"] = deployment_id
|
|
280
248
|
raw_vulns = get_sca_vulns(semgrep_app_token, deployment_slug)
|
|
281
249
|
vulns, usages = transform_sca_vulns(raw_vulns)
|
|
282
|
-
load_semgrep_sca_vulns(
|
|
283
|
-
load_semgrep_sca_usages(
|
|
284
|
-
run_scoped_analysis_job('semgrep_sca_risk_analysis.json',
|
|
285
|
-
|
|
250
|
+
load_semgrep_sca_vulns(neo4j_session, vulns, deployment_id, update_tag)
|
|
251
|
+
load_semgrep_sca_usages(neo4j_session, usages, deployment_id, update_tag)
|
|
252
|
+
run_scoped_analysis_job('semgrep_sca_risk_analysis.json', neo4j_session, common_job_parameters)
|
|
253
|
+
|
|
254
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
286
255
|
merge_module_sync_metadata(
|
|
287
|
-
neo4j_session=
|
|
256
|
+
neo4j_session=neo4j_session,
|
|
288
257
|
group_type='Semgrep',
|
|
289
258
|
group_id=deployment_id,
|
|
290
259
|
synced_type='SCA',
|
|
@@ -0,0 +1,97 @@
|
|
|
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 EC2NetworkAclRuleNodeProperties(CartographyNodeProperties):
|
|
17
|
+
id: PropertyRef = PropertyRef('Id')
|
|
18
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
19
|
+
network_acl_id: PropertyRef = PropertyRef('NetworkAclId')
|
|
20
|
+
protocol: PropertyRef = PropertyRef('Protocol')
|
|
21
|
+
fromport: PropertyRef = PropertyRef('FromPort')
|
|
22
|
+
toport: PropertyRef = PropertyRef('ToPort')
|
|
23
|
+
cidrblock: PropertyRef = PropertyRef('CidrBlock')
|
|
24
|
+
egress: PropertyRef = PropertyRef('Egress')
|
|
25
|
+
rulenumber: PropertyRef = PropertyRef('RuleNumber')
|
|
26
|
+
ruleaction: PropertyRef = PropertyRef('RuleAction')
|
|
27
|
+
region: PropertyRef = PropertyRef('Region', set_in_kwargs=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class EC2NetworkAclRuleAclRelProperties(CartographyRelProperties):
|
|
32
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class EC2NetworkAclRuleToAcl(CartographyRelSchema):
|
|
37
|
+
target_node_label: str = 'EC2NetworkAcl'
|
|
38
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
39
|
+
{'network_acl_id': PropertyRef('NetworkAclId')},
|
|
40
|
+
)
|
|
41
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
42
|
+
rel_label: str = "MEMBER_OF_NACL"
|
|
43
|
+
properties: EC2NetworkAclRuleAclRelProperties = EC2NetworkAclRuleAclRelProperties()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EC2NetworkAclRuleToAwsAccountRelProperties(CartographyRelProperties):
|
|
48
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class EC2NetworkAclRuleToAWSAccount(CartographyRelSchema):
|
|
53
|
+
target_node_label: str = 'AWSAccount'
|
|
54
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
55
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
56
|
+
)
|
|
57
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
58
|
+
rel_label: str = "RESOURCE"
|
|
59
|
+
properties: EC2NetworkAclRuleToAwsAccountRelProperties = EC2NetworkAclRuleToAwsAccountRelProperties()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EC2NetworkAclInboundRuleSchema(CartographyNodeSchema):
|
|
64
|
+
"""
|
|
65
|
+
Network interface as known by describe-network-interfaces.
|
|
66
|
+
"""
|
|
67
|
+
label: str = 'EC2NetworkAclRule'
|
|
68
|
+
extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(
|
|
69
|
+
['IpPermissionInbound'],
|
|
70
|
+
)
|
|
71
|
+
properties: EC2NetworkAclRuleNodeProperties = EC2NetworkAclRuleNodeProperties()
|
|
72
|
+
sub_resource_relationship: EC2NetworkAclRuleToAWSAccount = EC2NetworkAclRuleToAWSAccount()
|
|
73
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
74
|
+
[
|
|
75
|
+
EC2NetworkAclRuleToAcl(),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class EC2NetworkAclEgressRuleSchema(CartographyNodeSchema):
|
|
82
|
+
"""
|
|
83
|
+
Network interface as known by describe-network-interfaces.
|
|
84
|
+
"""
|
|
85
|
+
label: str = 'EC2NetworkAclRule'
|
|
86
|
+
extra_node_labels: ExtraNodeLabels = ExtraNodeLabels(
|
|
87
|
+
[
|
|
88
|
+
'IpPermissionEgress',
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
properties: EC2NetworkAclRuleNodeProperties = EC2NetworkAclRuleNodeProperties()
|
|
92
|
+
sub_resource_relationship: EC2NetworkAclRuleToAWSAccount = EC2NetworkAclRuleToAWSAccount()
|
|
93
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
94
|
+
[
|
|
95
|
+
EC2NetworkAclRuleToAcl(),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
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.relationships import CartographyRelProperties
|
|
7
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
8
|
+
from cartography.models.core.relationships import LinkDirection
|
|
9
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
10
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
11
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class EC2NetworkAclNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef('Arn')
|
|
17
|
+
arn: PropertyRef = PropertyRef('Arn')
|
|
18
|
+
network_acl_id: PropertyRef = PropertyRef('Id')
|
|
19
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
20
|
+
is_default: PropertyRef = PropertyRef('IsDefault')
|
|
21
|
+
region: PropertyRef = PropertyRef('Region', set_in_kwargs=True)
|
|
22
|
+
vpc_id: PropertyRef = PropertyRef('VpcId')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class EC2NetworkAclToVpcRelProperties(CartographyRelProperties):
|
|
27
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class EC2NetworkAclToVpc(CartographyRelSchema):
|
|
32
|
+
target_node_label: str = 'AWSVpc'
|
|
33
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
34
|
+
{'vpcid': PropertyRef('VpcId')},
|
|
35
|
+
)
|
|
36
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
37
|
+
rel_label: str = "MEMBER_OF_AWS_VPC"
|
|
38
|
+
properties: EC2NetworkAclToVpcRelProperties = EC2NetworkAclToVpcRelProperties()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EC2NetworkAclToSubnetRelProperties(CartographyRelProperties):
|
|
43
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class EC2NetworkAclToSubnet(CartographyRelSchema):
|
|
48
|
+
target_node_label: str = 'EC2Subnet'
|
|
49
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
50
|
+
{'subnetid': PropertyRef('SubnetId')},
|
|
51
|
+
)
|
|
52
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
53
|
+
rel_label: str = "PART_OF_SUBNET"
|
|
54
|
+
properties: EC2NetworkAclToSubnetRelProperties = EC2NetworkAclToSubnetRelProperties()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class EC2NetworkAclToAwsAccountRelProperties(CartographyRelProperties):
|
|
59
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EC2NetworkAclToAWSAccount(CartographyRelSchema):
|
|
64
|
+
target_node_label: str = 'AWSAccount'
|
|
65
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
66
|
+
{'id': PropertyRef('AWS_ID', set_in_kwargs=True)},
|
|
67
|
+
)
|
|
68
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
69
|
+
rel_label: str = "RESOURCE"
|
|
70
|
+
properties: EC2NetworkAclToAwsAccountRelProperties = EC2NetworkAclToAwsAccountRelProperties()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class EC2NetworkAclSchema(CartographyNodeSchema):
|
|
75
|
+
"""
|
|
76
|
+
Network interface as known by describe-network-interfaces.
|
|
77
|
+
"""
|
|
78
|
+
label: str = 'EC2NetworkAcl'
|
|
79
|
+
properties: EC2NetworkAclNodeProperties = EC2NetworkAclNodeProperties()
|
|
80
|
+
sub_resource_relationship: EC2NetworkAclToAWSAccount = EC2NetworkAclToAWSAccount()
|
|
81
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
82
|
+
[
|
|
83
|
+
EC2NetworkAclToVpc(),
|
|
84
|
+
EC2NetworkAclToSubnet(),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
@@ -8,7 +8,14 @@ class PropertyRef:
|
|
|
8
8
|
(PropertyRef.set_in_kwargs=True).
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
def __init__(
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
set_in_kwargs=False,
|
|
15
|
+
extra_index=False,
|
|
16
|
+
ignore_case=False,
|
|
17
|
+
fuzzy_and_ignore_case=False,
|
|
18
|
+
):
|
|
12
19
|
"""
|
|
13
20
|
:param name: The name of the property
|
|
14
21
|
:param set_in_kwargs: Optional. If True, the property is not defined on the data dict, and we expect to find the
|
|
@@ -33,11 +40,21 @@ class PropertyRef:
|
|
|
33
40
|
cartography catalog of GitHubUser nodes. Therefore, you would need `ignore_case=True` in the PropertyRef
|
|
34
41
|
that points to the GitHubUser node's name field, otherwise if one of your employees' GitHub usernames
|
|
35
42
|
contains capital letters, you would not be able to map them properly to a GitHubUser node in your graph.
|
|
43
|
+
:param fuzzy_and_ignore_case: If True, performs a fuzzy + case-insensitive match when comparing the value of
|
|
44
|
+
this property using the `CONTAINS` operator.
|
|
45
|
+
query. Defaults to False. This only has effect as part of a TargetNodeMatcher and is not supported for the
|
|
46
|
+
sub resource relationship.
|
|
36
47
|
"""
|
|
37
48
|
self.name = name
|
|
38
49
|
self.set_in_kwargs = set_in_kwargs
|
|
39
50
|
self.extra_index = extra_index
|
|
40
51
|
self.ignore_case = ignore_case
|
|
52
|
+
self.fuzzy_and_ignore_case = fuzzy_and_ignore_case
|
|
53
|
+
if self.fuzzy_and_ignore_case and self.ignore_case:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f'Error setting PropertyRef "{self.name}": ignore_case cannot be used together with'
|
|
56
|
+
'fuzzy_and_ignore_case. Pick one or the other.',
|
|
57
|
+
)
|
|
41
58
|
|
|
42
59
|
def _parameterize_name(self) -> str:
|
|
43
60
|
return f"${self.name}"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
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.nodes import ExtraNodeLabels
|
|
8
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
9
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
10
|
+
from cartography.models.core.relationships import LinkDirection
|
|
11
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
12
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
13
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class SemgrepDependencyNodeProperties(CartographyNodeProperties):
|
|
18
|
+
id: PropertyRef = PropertyRef('id')
|
|
19
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
20
|
+
name: PropertyRef = PropertyRef('name')
|
|
21
|
+
ecosystem: PropertyRef = PropertyRef('ecosystem')
|
|
22
|
+
version: PropertyRef = PropertyRef('version')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class SemgrepDependencyToSemgrepDeploymentRelProperties(CartographyRelProperties):
|
|
27
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
# (:SemgrepDependency)<-[:RESOURCE]-(:SemgrepDeployment)
|
|
32
|
+
class SemgrepDependencyToSemgrepDeploymentSchema(CartographyRelSchema):
|
|
33
|
+
target_node_label: str = 'SemgrepDeployment'
|
|
34
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
35
|
+
{'id': PropertyRef('DEPLOYMENT_ID', set_in_kwargs=True)},
|
|
36
|
+
)
|
|
37
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
38
|
+
rel_label: str = "RESOURCE"
|
|
39
|
+
properties: SemgrepDependencyToSemgrepDeploymentRelProperties = SemgrepDependencyToSemgrepDeploymentRelProperties()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SemgrepDependencyToGithubRepoRelProperties(CartographyRelProperties):
|
|
44
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
45
|
+
specifier: PropertyRef = PropertyRef('specifier')
|
|
46
|
+
transitivity: PropertyRef = PropertyRef('transitivity')
|
|
47
|
+
url: PropertyRef = PropertyRef('url')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
# (:SemgrepDependency)<-[:REQUIRES]-(:GitHubRepository)
|
|
52
|
+
class SemgrepDependencyToGithubRepoRel(CartographyRelSchema):
|
|
53
|
+
target_node_label: str = 'GitHubRepository'
|
|
54
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
55
|
+
{'id': PropertyRef('repo_url')},
|
|
56
|
+
)
|
|
57
|
+
direction: LinkDirection = LinkDirection.INWARD
|
|
58
|
+
rel_label: str = "REQUIRES"
|
|
59
|
+
properties: SemgrepDependencyToGithubRepoRelProperties = SemgrepDependencyToGithubRepoRelProperties()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class SemgrepSCAFindngToDependencyRelProperties(CartographyRelProperties):
|
|
64
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class SemgrepGoLibrarySchema(CartographyNodeSchema):
|
|
69
|
+
label: str = 'GoLibrary'
|
|
70
|
+
extra_node_labels: Optional[ExtraNodeLabels] = ExtraNodeLabels(['Dependency', 'SemgrepDependency'])
|
|
71
|
+
properties: SemgrepDependencyNodeProperties = SemgrepDependencyNodeProperties()
|
|
72
|
+
sub_resource_relationship: SemgrepDependencyToSemgrepDeploymentSchema = SemgrepDependencyToSemgrepDeploymentSchema()
|
|
73
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
74
|
+
[
|
|
75
|
+
SemgrepDependencyToGithubRepoRel(),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.96.0rc1
|
|
4
4
|
Summary: Explore assets and their relationships across your technical infrastructure.
|
|
5
5
|
Home-page: https://www.github.com/cartography-cncf/cartography
|
|
6
6
|
Maintainer: Cartography Contributors
|
|
@@ -135,7 +135,7 @@ cartography/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
135
135
|
cartography/graph/cleanupbuilder.py,sha256=87vFrOJo66hOrrqeNwXp18WrNQEheHTlZko9KUkXWhY,8021
|
|
136
136
|
cartography/graph/context.py,sha256=RGxGb8EnxowcqjR0nFF86baNhgRHeUF9wjIoFUoG8LU,1230
|
|
137
137
|
cartography/graph/job.py,sha256=RZWsbNhHuJlcSpw4C73ZuovRTp7kGrcm3X9yUH8vT1Q,7488
|
|
138
|
-
cartography/graph/querybuilder.py,sha256=
|
|
138
|
+
cartography/graph/querybuilder.py,sha256=7DtIGPlfeKcIWKw_ReCAX1OeUkhSnIRV_reeVaKL6I4,20416
|
|
139
139
|
cartography/graph/statement.py,sha256=VsqG46ty_Mm87fr8YdIwfr6a82OUXU7yZe6S-Py9hZg,5345
|
|
140
140
|
cartography/intel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
141
141
|
cartography/intel/analysis.py,sha256=gHtN42NqqLL1G5MOm2Q6rMyg-V5lU_wqbnKp5hbOOao,1499
|
|
@@ -160,7 +160,7 @@ cartography/intel/aws/permission_relationships.py,sha256=IarV9gt5BaplZ5TPo_mfypt
|
|
|
160
160
|
cartography/intel/aws/rds.py,sha256=vnlNYmrO2Cc0PNn31CeG2QwYhwjVosbQFE9Ol1vQyLE,25252
|
|
161
161
|
cartography/intel/aws/redshift.py,sha256=KOqiXIllHmtPTeaNGl-cX4srY5pFE6o12j8MQ5-zWpc,6694
|
|
162
162
|
cartography/intel/aws/resourcegroupstaggingapi.py,sha256=aq4kPF6t8QZZoTxdkQVLXH65Di41CDJVM9llJNe6iaY,10278
|
|
163
|
-
cartography/intel/aws/resources.py,sha256=
|
|
163
|
+
cartography/intel/aws/resources.py,sha256=W-Psy1bjszIZ4dpsm_JPIMEeNzgWJSBu71hju3OUh8c,3272
|
|
164
164
|
cartography/intel/aws/route53.py,sha256=IYqeQud1HuHnf11A7T-Jeif5DWgjpaaU-Jfr2cLUc_o,14099
|
|
165
165
|
cartography/intel/aws/s3.py,sha256=SVxUMtMSkbdjZv5qOSYIbYb8BQa-QTojbHG85-EFWLA,27034
|
|
166
166
|
cartography/intel/aws/secretsmanager.py,sha256=YogwRPT6qZPVg5HrND71zI-nNn60oxoWaW7eUlhuTS0,3304
|
|
@@ -177,6 +177,7 @@ cartography/intel/aws/ec2/key_pairs.py,sha256=SvRgd56vE4eouvTSNoFK8PP8HYoECO91go
|
|
|
177
177
|
cartography/intel/aws/ec2/launch_templates.py,sha256=aeqaL8On38ET8nM8bISsIXLy6PkZoV-tqSWG38YXgkI,6010
|
|
178
178
|
cartography/intel/aws/ec2/load_balancer_v2s.py,sha256=95FfQQn740gexINIHDJizOM4OKzRtQT_y2XQMipQ5Dg,8661
|
|
179
179
|
cartography/intel/aws/ec2/load_balancers.py,sha256=1GwErzGqi3BKCARqfGJcD_r_D84rFKVy5kNMas9jAok,6756
|
|
180
|
+
cartography/intel/aws/ec2/network_acls.py,sha256=t8kQpBX7wRPumyF8Njx0o8E6rwmXd0DRNGoopgT9we0,6741
|
|
180
181
|
cartography/intel/aws/ec2/network_interfaces.py,sha256=CzF8PooCYUQ2pk8DR8JDAhkWRUQSBj_27OsIfkL_-Cs,9199
|
|
181
182
|
cartography/intel/aws/ec2/reserved_instances.py,sha256=jv8-VLI5KL8jN1QRI20yim8lzZ7I7wR8a5EF8DckahA,3122
|
|
182
183
|
cartography/intel/aws/ec2/security_groups.py,sha256=vxLeaCpCowkbl-YpON1UdbjtPolMfj_reOEuKujN80Y,6060
|
|
@@ -267,8 +268,10 @@ cartography/intel/pagerduty/services.py,sha256=Cjm37mWmuBNXSY49-xUQ3xV0DZ391GTLv
|
|
|
267
268
|
cartography/intel/pagerduty/teams.py,sha256=aRubUXgEVVReyLrXAX_be1E_QBJv3Qlr4n779Jkkz8Q,2498
|
|
268
269
|
cartography/intel/pagerduty/users.py,sha256=oltGssxrnzYsV6QTGP1SsPoA1rCUDStj6vGlGWY695g,1623
|
|
269
270
|
cartography/intel/pagerduty/vendors.py,sha256=WlDHExrWRBegDQKtxBV5nJiYgwoTLxNee4HrQDJ-Pdg,1559
|
|
270
|
-
cartography/intel/semgrep/__init__.py,sha256=
|
|
271
|
-
cartography/intel/semgrep/
|
|
271
|
+
cartography/intel/semgrep/__init__.py,sha256=XhfixZPJXBIA9SRQDxe2wY26KW447lTPR0Q_DV7aFTU,1150
|
|
272
|
+
cartography/intel/semgrep/dependencies.py,sha256=2Uq86WLDlfO-lNPot__z0TXPbe_cXC_5HFLyYqfbVKU,6742
|
|
273
|
+
cartography/intel/semgrep/deployment.py,sha256=sh-yJHtdgZjxIJimNnA-DxsM-MYgMhQ_WX2E7w4PWiM,2012
|
|
274
|
+
cartography/intel/semgrep/findings.py,sha256=GDqZmfl9-HiZ93u0jhlkZ1w_qzrD1cFFFJtVc7DFdPk,9761
|
|
272
275
|
cartography/intel/snipeit/__init__.py,sha256=0uIh8NbuI7IbfgaOrPHg4Nfm1yO6mTRC_qaFiIjR2FA,992
|
|
273
276
|
cartography/intel/snipeit/asset.py,sha256=KkGRUgIydvf_6SHtgpVLT-TjtEGz029SrOaoh0qDW6E,1997
|
|
274
277
|
cartography/intel/snipeit/user.py,sha256=hm9v_p29bphHtGe9LKVo1FD_rQcbCigrCRf8YsmteXA,1971
|
|
@@ -286,6 +289,8 @@ cartography/models/aws/ec2/keypairs.py,sha256=scKC3SdExHAWkPNmb6tT9LK-9q4sweqS2e
|
|
|
286
289
|
cartography/models/aws/ec2/launch_template_versions.py,sha256=RitfnAuAj0XpFsCXkRbtUhHMAi8Vsvmtury231eKvGU,3897
|
|
287
290
|
cartography/models/aws/ec2/launch_templates.py,sha256=GqiwFuMp72LNSt2eQlp2WfdU_vHsom-xKV5AaUewSHQ,2157
|
|
288
291
|
cartography/models/aws/ec2/loadbalancerv2.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
292
|
+
cartography/models/aws/ec2/network_acl_rules.py,sha256=anzznH-WVXoZk2zwoXRhHUcIPvrsCYfi2fqAhhSLdW4,3889
|
|
293
|
+
cartography/models/aws/ec2/network_acls.py,sha256=pJKsXdMLB8L79lmTYpLJfFJ6p7PWpf3rBN6eW6y-5hY,3419
|
|
289
294
|
cartography/models/aws/ec2/networkinterface_instance.py,sha256=t3oqcQ4GjYf7dwqPUGCiXd70ie4ibYLilOXiE5_Ad8g,4707
|
|
290
295
|
cartography/models/aws/ec2/networkinterfaces.py,sha256=z1-Dl6I79-TCxXKG8QBpSKga93lPCPaLR1XqKJZK3ME,4127
|
|
291
296
|
cartography/models/aws/ec2/privateip_networkinterface.py,sha256=j8MyiZsiUCuzuGUH_4PBKV3rLTk1GkE-SZb6K11oSdM,3038
|
|
@@ -307,7 +312,7 @@ cartography/models/bigfix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
307
312
|
cartography/models/bigfix/bigfix_computer.py,sha256=HQQsQPUphfkBsW8jIP0b9InNAb3vtOSWVD_GqSikkm4,3520
|
|
308
313
|
cartography/models/bigfix/bigfix_root.py,sha256=GkyI0Gmnat8Q-3aQWJdCaNzKB89fY0ee4-lRvvPRLns,598
|
|
309
314
|
cartography/models/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
310
|
-
cartography/models/core/common.py,sha256=
|
|
315
|
+
cartography/models/core/common.py,sha256=uAMmUAY7_ENDsq-CvEZ0bQNAwdWOFRCzIQEFz7NOtmI,4316
|
|
311
316
|
cartography/models/core/nodes.py,sha256=h5dwBOk_a2uCHZWeQz3pidr7gkqMKf7buIZgl6M1Ox4,3699
|
|
312
317
|
cartography/models/core/relationships.py,sha256=6AwXvk0dq48BxqyxBpHyBXZ3dJNm65t1y4vNg4n25uA,5103
|
|
313
318
|
cartography/models/cve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -330,6 +335,7 @@ cartography/models/lastpass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
330
335
|
cartography/models/lastpass/tenant.py,sha256=TG-9LFo9Sfzb9UgcTt_gFVTKocLItbgQMMPkN_iprXU,618
|
|
331
336
|
cartography/models/lastpass/user.py,sha256=SMTTYN6jgccc9k76hY3rVImElJOhHhZ9f1aZ6JzcrHw,3487
|
|
332
337
|
cartography/models/semgrep/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
338
|
+
cartography/models/semgrep/dependencies.py,sha256=QcvlqKKvX1bi4iqcM0ioYb1ZfGfisJ9E2o2CMnEf2CE,3399
|
|
333
339
|
cartography/models/semgrep/deployment.py,sha256=or5qZDuR51MXzINpH15jZrqmSUvXQevCNYWJ7D6v-JI,745
|
|
334
340
|
cartography/models/semgrep/findings.py,sha256=RPd-QzvP38fbTIqFARx6XpcZSsd5JM3KIg-ZlJA7NlE,5490
|
|
335
341
|
cartography/models/semgrep/locations.py,sha256=kSk7Nn5Mn4Ob84MVZOo2GR0YFi-9Okq9pgA3FfC6_bk,3061
|
|
@@ -337,9 +343,9 @@ cartography/models/snipeit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
337
343
|
cartography/models/snipeit/asset.py,sha256=FyRAaeXuZjMy0eUQcSDFcgEAF5lbLMlvqp1Tv9d3Lv4,3238
|
|
338
344
|
cartography/models/snipeit/tenant.py,sha256=p4rFnpNNuF1W5ilGBbexDaETWTwavfb38RcQGoImkQI,679
|
|
339
345
|
cartography/models/snipeit/user.py,sha256=MsB4MiCVNTH6JpESime7cOkB89autZOXQpL6Z0l7L6o,2113
|
|
340
|
-
cartography-0.
|
|
341
|
-
cartography-0.
|
|
342
|
-
cartography-0.
|
|
343
|
-
cartography-0.
|
|
344
|
-
cartography-0.
|
|
345
|
-
cartography-0.
|
|
346
|
+
cartography-0.96.0rc1.dist-info/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
|
|
347
|
+
cartography-0.96.0rc1.dist-info/METADATA,sha256=XD5vv3n7YJIVKQjrySebeVAG-xABJUaaner8ELB2NG0,1966
|
|
348
|
+
cartography-0.96.0rc1.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
349
|
+
cartography-0.96.0rc1.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
|
|
350
|
+
cartography-0.96.0rc1.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
|
|
351
|
+
cartography-0.96.0rc1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|