cartography 0.96.0rc1__py3-none-any.whl → 0.96.0rc2__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/cli.py +15 -0
- cartography/config.py +4 -0
- cartography/data/indexes.cypher +1 -2
- cartography/data/jobs/cleanup/aws_import_identity_center_cleanup.json +16 -0
- cartography/data/jobs/cleanup/{github_users_cleanup.json → github_org_and_users_cleanup.json} +5 -0
- cartography/intel/aws/identitycenter.py +307 -0
- cartography/intel/aws/resources.py +2 -0
- cartography/intel/github/users.py +156 -39
- cartography/intel/okta/users.py +2 -1
- cartography/intel/semgrep/__init__.py +1 -1
- cartography/intel/semgrep/dependencies.py +54 -22
- cartography/models/github/orgs.py +26 -0
- cartography/models/github/users.py +119 -0
- cartography/models/semgrep/dependencies.py +13 -0
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/METADATA +1 -1
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/RECORD +20 -16
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/LICENSE +0 -0
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/WHEEL +0 -0
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/entry_points.txt +0 -0
- {cartography-0.96.0rc1.dist-info → cartography-0.96.0rc2.dist-info}/top_level.txt +0 -0
cartography/cli.py
CHANGED
|
@@ -9,6 +9,7 @@ import cartography.config
|
|
|
9
9
|
import cartography.sync
|
|
10
10
|
import cartography.util
|
|
11
11
|
from cartography.intel.aws.util.common import parse_and_validate_aws_requested_syncs
|
|
12
|
+
from cartography.intel.semgrep.dependencies import parse_and_validate_semgrep_ecosystems
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
@@ -524,6 +525,17 @@ class CLI:
|
|
|
524
525
|
'Required if you are using the Semgrep intel module. Ignored otherwise.'
|
|
525
526
|
),
|
|
526
527
|
)
|
|
528
|
+
parser.add_argument(
|
|
529
|
+
'--semgrep-dependency-ecosystems',
|
|
530
|
+
type=str,
|
|
531
|
+
default=None,
|
|
532
|
+
help=(
|
|
533
|
+
'Comma-separated list of language ecosystems for which dependencies will be retrieved from Semgrep. '
|
|
534
|
+
'For example, a value of "gomod,npm" will retrieve Go and NPM dependencies. '
|
|
535
|
+
'See the full list of supported ecosystems in source code at cartography.intel.semgrep.dependencies. '
|
|
536
|
+
'Required if you are using the Semgrep dependencies intel module. Ignored otherwise.'
|
|
537
|
+
),
|
|
538
|
+
)
|
|
527
539
|
parser.add_argument(
|
|
528
540
|
'--snipeit-base-uri',
|
|
529
541
|
type=str,
|
|
@@ -734,6 +746,9 @@ class CLI:
|
|
|
734
746
|
config.semgrep_app_token = os.environ.get(config.semgrep_app_token_env_var)
|
|
735
747
|
else:
|
|
736
748
|
config.semgrep_app_token = None
|
|
749
|
+
if config.semgrep_dependency_ecosystems:
|
|
750
|
+
# No need to store the returned value; we're using this for input validation.
|
|
751
|
+
parse_and_validate_semgrep_ecosystems(config.semgrep_dependency_ecosystems)
|
|
737
752
|
|
|
738
753
|
# CVE feed config
|
|
739
754
|
if config.cve_api_key_env_var:
|
cartography/config.py
CHANGED
|
@@ -107,6 +107,8 @@ class Config:
|
|
|
107
107
|
:param duo_api_hostname: The Duo api hostname, e.g. "api-abc123.duosecurity.com". Optional.
|
|
108
108
|
:param semgrep_app_token: The Semgrep api token. Optional.
|
|
109
109
|
:type semgrep_app_token: str
|
|
110
|
+
:param semgrep_dependency_ecosystems: Comma-separated list of Semgrep dependency ecosystems to fetch. Optional.
|
|
111
|
+
:type semgrep_dependency_ecosystems: str
|
|
110
112
|
:type snipeit_base_uri: string
|
|
111
113
|
:param snipeit_base_uri: SnipeIT data provider base URI. Optional.
|
|
112
114
|
:type snipeit_token: string
|
|
@@ -170,6 +172,7 @@ class Config:
|
|
|
170
172
|
duo_api_secret=None,
|
|
171
173
|
duo_api_hostname=None,
|
|
172
174
|
semgrep_app_token=None,
|
|
175
|
+
semgrep_dependency_ecosystems=None,
|
|
173
176
|
snipeit_base_uri=None,
|
|
174
177
|
snipeit_token=None,
|
|
175
178
|
snipeit_tenant_id=None,
|
|
@@ -227,6 +230,7 @@ class Config:
|
|
|
227
230
|
self.duo_api_secret = duo_api_secret
|
|
228
231
|
self.duo_api_hostname = duo_api_hostname
|
|
229
232
|
self.semgrep_app_token = semgrep_app_token
|
|
233
|
+
self.semgrep_dependency_ecosystems = semgrep_dependency_ecosystems
|
|
230
234
|
self.snipeit_base_uri = snipeit_base_uri
|
|
231
235
|
self.snipeit_token = snipeit_token
|
|
232
236
|
self.snipeit_tenant_id = snipeit_tenant_id
|
cartography/data/indexes.cypher
CHANGED
|
@@ -305,8 +305,7 @@ CREATE INDEX IF NOT EXISTS FOR (n:SpotlightVulnerability) ON (n.host_info_local_
|
|
|
305
305
|
CREATE INDEX IF NOT EXISTS FOR (n:SpotlightVulnerability) ON (n.lastupdated);
|
|
306
306
|
CREATE INDEX IF NOT EXISTS FOR (n:SQSQueue) ON (n.id);
|
|
307
307
|
CREATE INDEX IF NOT EXISTS FOR (n:SQSQueue) ON (n.lastupdated);
|
|
308
|
-
CREATE INDEX IF NOT EXISTS FOR (n:
|
|
309
|
-
CREATE INDEX IF NOT EXISTS FOR (n:User) ON (n.lastupdated);
|
|
308
|
+
CREATE INDEX IF NOT EXISTS FOR (n:UserAccount) ON (n.id);
|
|
310
309
|
CREATE INDEX IF NOT EXISTS FOR (n:AzureTenant) ON (n.id);
|
|
311
310
|
CREATE INDEX IF NOT EXISTS FOR (n:AzureTenant) ON (n.lastupdated);
|
|
312
311
|
CREATE INDEX IF NOT EXISTS FOR (n:AzurePrincipal) ON (n.email);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"statements": [
|
|
3
|
+
|
|
4
|
+
{
|
|
5
|
+
"query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:AWSSSOUser)<-[r:CAN_ASSUME_IDENTITY]-(:OktaUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r) RETURN COUNT(*) as TotalDeleted",
|
|
6
|
+
"iterative": true,
|
|
7
|
+
"iterationsize": 100
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"query": "MATCH (:AWSAccount{id: $AWS_ID})-[:RESOURCE]->(:AWSRole)-[r:ALLOWED_BY]->(:AWSSSOUser) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r) RETURN COUNT(*) as TotalDeleted",
|
|
11
|
+
"iterative": true,
|
|
12
|
+
"iterationsize": 100
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"name": "cleanup AWS Identity Center Instances and Related Data"
|
|
16
|
+
}
|
cartography/data/jobs/cleanup/{github_users_cleanup.json → github_org_and_users_cleanup.json}
RENAMED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"query": "MATCH (:GitHubUser)-[r:MEMBER_OF]->(:GitHubOrganization) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)",
|
|
19
19
|
"iterative": true,
|
|
20
20
|
"iterationsize": 100
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "MATCH (:GitHubUser)-[r:UNAFFILIATED]->(:GitHubOrganization) WHERE r.lastupdated <> $UPDATE_TAG WITH r LIMIT $LIMIT_SIZE DELETE (r)",
|
|
24
|
+
"iterative": true,
|
|
25
|
+
"iterationsize": 100
|
|
21
26
|
}],
|
|
22
27
|
"name": "cleanup GitHub users data"
|
|
23
28
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import Dict
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
import neo4j
|
|
8
|
+
|
|
9
|
+
from cartography.client.core.tx import load
|
|
10
|
+
from cartography.graph.job import GraphJob
|
|
11
|
+
from cartography.models.aws.identitycenter.awsidentitycenter import AWSIdentityCenterInstanceSchema
|
|
12
|
+
from cartography.models.aws.identitycenter.awspermissionset import AWSPermissionSetSchema
|
|
13
|
+
from cartography.models.aws.identitycenter.awsssouser import AWSSSOUserSchema
|
|
14
|
+
from cartography.util import aws_handle_regions
|
|
15
|
+
from cartography.util import run_cleanup_job
|
|
16
|
+
from cartography.util import timeit
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@timeit
|
|
21
|
+
@aws_handle_regions
|
|
22
|
+
def get_identity_center_instances(boto3_session: boto3.session.Session, region: str) -> List[Dict]:
|
|
23
|
+
"""
|
|
24
|
+
Get all AWS IAM Identity Center instances in the current region
|
|
25
|
+
"""
|
|
26
|
+
client = boto3_session.client('sso-admin', region_name=region)
|
|
27
|
+
instances = []
|
|
28
|
+
|
|
29
|
+
paginator = client.get_paginator('list_instances')
|
|
30
|
+
for page in paginator.paginate():
|
|
31
|
+
instances.extend(page.get('Instances', []))
|
|
32
|
+
|
|
33
|
+
return instances
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@timeit
|
|
37
|
+
def load_identity_center_instances(
|
|
38
|
+
neo4j_session: neo4j.Session,
|
|
39
|
+
instance_data: List[Dict],
|
|
40
|
+
region: str,
|
|
41
|
+
current_aws_account_id: str,
|
|
42
|
+
aws_update_tag: int,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Load Identity Center instances into the graph
|
|
46
|
+
"""
|
|
47
|
+
logger.info(f"Loading {len(instance_data)} Identity Center instances for region {region}")
|
|
48
|
+
load(
|
|
49
|
+
neo4j_session,
|
|
50
|
+
AWSIdentityCenterInstanceSchema(),
|
|
51
|
+
instance_data,
|
|
52
|
+
lastupdated=aws_update_tag,
|
|
53
|
+
Region=region,
|
|
54
|
+
AWS_ID=current_aws_account_id,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@timeit
|
|
59
|
+
def get_permission_sets(boto3_session: boto3.session.Session, instance_arn: str, region: str) -> List[Dict]:
|
|
60
|
+
"""
|
|
61
|
+
Get all permission sets for a given Identity Center instance
|
|
62
|
+
"""
|
|
63
|
+
client = boto3_session.client('sso-admin', region_name=region)
|
|
64
|
+
permission_sets = []
|
|
65
|
+
|
|
66
|
+
paginator = client.get_paginator('list_permission_sets')
|
|
67
|
+
for page in paginator.paginate(InstanceArn=instance_arn):
|
|
68
|
+
# Get detailed info for each permission set
|
|
69
|
+
for arn in page.get('PermissionSets', []):
|
|
70
|
+
details = client.describe_permission_set(
|
|
71
|
+
InstanceArn=instance_arn,
|
|
72
|
+
PermissionSetArn=arn,
|
|
73
|
+
)
|
|
74
|
+
permission_set = details.get('PermissionSet', {})
|
|
75
|
+
if permission_set:
|
|
76
|
+
permission_set['RoleHint'] = (
|
|
77
|
+
f":role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_{permission_set.get('Name')}"
|
|
78
|
+
)
|
|
79
|
+
permission_sets.append(permission_set)
|
|
80
|
+
|
|
81
|
+
return permission_sets
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@timeit
|
|
85
|
+
def get_permission_set_roles(
|
|
86
|
+
boto3_session: boto3.session.Session,
|
|
87
|
+
instance_arn: str,
|
|
88
|
+
permission_set_arn: str,
|
|
89
|
+
region: str,
|
|
90
|
+
) -> List[Dict]:
|
|
91
|
+
"""
|
|
92
|
+
Get all accounts associated with a given permission set
|
|
93
|
+
"""
|
|
94
|
+
client = boto3_session.client('sso-admin', region_name=region)
|
|
95
|
+
accounts = []
|
|
96
|
+
|
|
97
|
+
paginator = client.get_paginator('list_accounts_for_provisioned_permission_set')
|
|
98
|
+
for page in paginator.paginate(InstanceArn=instance_arn, PermissionSetArn=permission_set_arn):
|
|
99
|
+
accounts.extend(page.get('AccountIds', []))
|
|
100
|
+
|
|
101
|
+
return accounts
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@timeit
|
|
105
|
+
def load_permission_sets(
|
|
106
|
+
neo4j_session: neo4j.Session,
|
|
107
|
+
permission_sets: List[Dict],
|
|
108
|
+
instance_arn: str,
|
|
109
|
+
region: str,
|
|
110
|
+
aws_account_id: str,
|
|
111
|
+
aws_update_tag: int,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Load Identity Center permission sets into the graph
|
|
115
|
+
"""
|
|
116
|
+
logger.info(f"Loading {len(permission_sets)} permission sets for instance {instance_arn} in region {region}")
|
|
117
|
+
|
|
118
|
+
load(
|
|
119
|
+
neo4j_session,
|
|
120
|
+
AWSPermissionSetSchema(),
|
|
121
|
+
permission_sets,
|
|
122
|
+
lastupdated=aws_update_tag,
|
|
123
|
+
InstanceArn=instance_arn,
|
|
124
|
+
Region=region,
|
|
125
|
+
AWS_ID=aws_account_id,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@timeit
|
|
130
|
+
def get_sso_users(
|
|
131
|
+
boto3_session: boto3.session.Session,
|
|
132
|
+
identity_store_id: str,
|
|
133
|
+
region: str,
|
|
134
|
+
) -> List[Dict]:
|
|
135
|
+
"""
|
|
136
|
+
Get all SSO users for a given Identity Store
|
|
137
|
+
"""
|
|
138
|
+
client = boto3_session.client('identitystore', region_name=region)
|
|
139
|
+
users = []
|
|
140
|
+
|
|
141
|
+
paginator = client.get_paginator('list_users')
|
|
142
|
+
for page in paginator.paginate(IdentityStoreId=identity_store_id):
|
|
143
|
+
user_page = page.get('Users', [])
|
|
144
|
+
for user in user_page:
|
|
145
|
+
if user.get('ExternalIds', None):
|
|
146
|
+
user['ExternalId'] = user.get('ExternalIds')[0].get('Id')
|
|
147
|
+
users.append(user)
|
|
148
|
+
|
|
149
|
+
return users
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@timeit
|
|
153
|
+
def load_sso_users(
|
|
154
|
+
neo4j_session: neo4j.Session,
|
|
155
|
+
users: List[Dict],
|
|
156
|
+
identity_store_id: str,
|
|
157
|
+
region: str,
|
|
158
|
+
aws_account_id: str,
|
|
159
|
+
aws_update_tag: int,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Load SSO users into the graph
|
|
163
|
+
"""
|
|
164
|
+
logger.info(f"Loading {len(users)} SSO users for identity store {identity_store_id} in region {region}")
|
|
165
|
+
|
|
166
|
+
load(
|
|
167
|
+
neo4j_session,
|
|
168
|
+
AWSSSOUserSchema(),
|
|
169
|
+
users,
|
|
170
|
+
lastupdated=aws_update_tag,
|
|
171
|
+
IdentityStoreId=identity_store_id,
|
|
172
|
+
AWS_ID=aws_account_id,
|
|
173
|
+
Region=region,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@timeit
|
|
178
|
+
def get_role_assignments(
|
|
179
|
+
boto3_session: boto3.session.Session,
|
|
180
|
+
users: List[Dict],
|
|
181
|
+
instance_arn: str,
|
|
182
|
+
region: str,
|
|
183
|
+
) -> List[Dict]:
|
|
184
|
+
"""
|
|
185
|
+
Get role assignments for SSO users
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
logger.info(f"Getting role assignments for {len(users)} users")
|
|
189
|
+
client = boto3_session.client('sso-admin', region_name=region)
|
|
190
|
+
role_assignments = []
|
|
191
|
+
|
|
192
|
+
for user in users:
|
|
193
|
+
user_id = user['UserId']
|
|
194
|
+
paginator = client.get_paginator('list_account_assignments_for_principal')
|
|
195
|
+
for page in paginator.paginate(InstanceArn=instance_arn, PrincipalId=user_id, PrincipalType='USER'):
|
|
196
|
+
for assignment in page.get('AccountAssignments', []):
|
|
197
|
+
role_assignments.append({
|
|
198
|
+
'UserId': user_id,
|
|
199
|
+
'PermissionSetArn': assignment.get('PermissionSetArn'),
|
|
200
|
+
'AccountId': assignment.get('AccountId'),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
return role_assignments
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@timeit
|
|
207
|
+
def load_role_assignments(
|
|
208
|
+
neo4j_session: neo4j.Session,
|
|
209
|
+
role_assignments: List[Dict],
|
|
210
|
+
aws_update_tag: int,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Load role assignments into the graph
|
|
214
|
+
"""
|
|
215
|
+
logger.info(f"Loading {len(role_assignments)} role assignments")
|
|
216
|
+
if role_assignments:
|
|
217
|
+
neo4j_session.run(
|
|
218
|
+
"""
|
|
219
|
+
UNWIND $role_assignments AS ra
|
|
220
|
+
MATCH (acc:AWSAccount{id:ra.AccountId}) -[:RESOURCE]->
|
|
221
|
+
(role:AWSRole)<-[:ASSIGNED_TO_ROLE]-
|
|
222
|
+
(permset:AWSPermissionSet {id: ra.PermissionSetArn})
|
|
223
|
+
MATCH (sso:AWSSSOUser {id: ra.UserId})
|
|
224
|
+
MERGE (role)-[r:ALLOWED_BY]->(sso)
|
|
225
|
+
SET r.lastupdated = $aws_update_tag,
|
|
226
|
+
r.permission_set_arn = ra.PermissionSetArn
|
|
227
|
+
""",
|
|
228
|
+
role_assignments=role_assignments,
|
|
229
|
+
aws_update_tag=aws_update_tag,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
|
|
234
|
+
GraphJob.from_node_schema(AWSIdentityCenterInstanceSchema(), common_job_parameters).run(neo4j_session)
|
|
235
|
+
GraphJob.from_node_schema(AWSPermissionSetSchema(), common_job_parameters).run(neo4j_session)
|
|
236
|
+
GraphJob.from_node_schema(AWSSSOUserSchema(), common_job_parameters).run(neo4j_session)
|
|
237
|
+
run_cleanup_job(
|
|
238
|
+
'aws_import_identity_center_cleanup.json',
|
|
239
|
+
neo4j_session,
|
|
240
|
+
common_job_parameters,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@timeit
|
|
245
|
+
def sync_identity_center_instances(
|
|
246
|
+
neo4j_session: neo4j.Session,
|
|
247
|
+
boto3_session: boto3.session.Session,
|
|
248
|
+
regions: List[str],
|
|
249
|
+
current_aws_account_id: str,
|
|
250
|
+
update_tag: int,
|
|
251
|
+
common_job_parameters: Dict,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""
|
|
254
|
+
Sync Identity Center instances, their permission sets, and SSO users
|
|
255
|
+
"""
|
|
256
|
+
logger.info(f"Syncing Identity Center instances for regions {regions}")
|
|
257
|
+
for region in regions:
|
|
258
|
+
logger.info(f"Syncing Identity Center instances for region {region}")
|
|
259
|
+
instances = get_identity_center_instances(boto3_session, region)
|
|
260
|
+
load_identity_center_instances(
|
|
261
|
+
neo4j_session,
|
|
262
|
+
instances,
|
|
263
|
+
region,
|
|
264
|
+
current_aws_account_id,
|
|
265
|
+
update_tag,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# For each instance, get and load its permission sets and SSO users
|
|
269
|
+
for instance in instances:
|
|
270
|
+
instance_arn = instance['InstanceArn']
|
|
271
|
+
identity_store_id = instance['IdentityStoreId']
|
|
272
|
+
|
|
273
|
+
permission_sets = get_permission_sets(boto3_session, instance_arn, region)
|
|
274
|
+
|
|
275
|
+
load_permission_sets(
|
|
276
|
+
neo4j_session,
|
|
277
|
+
permission_sets,
|
|
278
|
+
instance_arn,
|
|
279
|
+
region,
|
|
280
|
+
current_aws_account_id,
|
|
281
|
+
update_tag,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
users = get_sso_users(boto3_session, identity_store_id, region)
|
|
285
|
+
load_sso_users(
|
|
286
|
+
neo4j_session,
|
|
287
|
+
users,
|
|
288
|
+
identity_store_id,
|
|
289
|
+
region,
|
|
290
|
+
current_aws_account_id,
|
|
291
|
+
update_tag,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Get and load role assignments
|
|
295
|
+
role_assignments = get_role_assignments(
|
|
296
|
+
boto3_session,
|
|
297
|
+
users,
|
|
298
|
+
instance_arn,
|
|
299
|
+
region,
|
|
300
|
+
)
|
|
301
|
+
load_role_assignments(
|
|
302
|
+
neo4j_session,
|
|
303
|
+
role_assignments,
|
|
304
|
+
update_tag,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
cleanup(neo4j_session, common_job_parameters)
|
|
@@ -10,6 +10,7 @@ from . import elasticache
|
|
|
10
10
|
from . import elasticsearch
|
|
11
11
|
from . import emr
|
|
12
12
|
from . import iam
|
|
13
|
+
from . import identitycenter
|
|
13
14
|
from . import inspector
|
|
14
15
|
from . import kms
|
|
15
16
|
from . import lambda_function
|
|
@@ -88,4 +89,5 @@ RESOURCE_FUNCTIONS: Dict = {
|
|
|
88
89
|
'ssm': ssm.sync,
|
|
89
90
|
'inspector': inspector.sync,
|
|
90
91
|
'config': config.sync,
|
|
92
|
+
'identitycenter': identitycenter.sync_identity_center_instances,
|
|
91
93
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from copy import deepcopy
|
|
2
3
|
from typing import Any
|
|
3
4
|
from typing import Dict
|
|
4
5
|
from typing import List
|
|
@@ -6,7 +7,11 @@ from typing import Tuple
|
|
|
6
7
|
|
|
7
8
|
import neo4j
|
|
8
9
|
|
|
10
|
+
from cartography.client.core.tx import load
|
|
9
11
|
from cartography.intel.github.util import fetch_all
|
|
12
|
+
from cartography.models.github.orgs import GitHubOrganizationSchema
|
|
13
|
+
from cartography.models.github.users import GitHubOrganizationUserSchema
|
|
14
|
+
from cartography.models.github.users import GitHubUnaffiliatedUserSchema
|
|
10
15
|
from cartography.stats import get_stats_client
|
|
11
16
|
from cartography.util import merge_module_sync_metadata
|
|
12
17
|
from cartography.util import run_cleanup_job
|
|
@@ -44,17 +49,46 @@ GITHUB_ORG_USERS_PAGINATED_GRAPHQL = """
|
|
|
44
49
|
}
|
|
45
50
|
"""
|
|
46
51
|
|
|
52
|
+
GITHUB_ENTERPRISE_OWNER_USERS_PAGINATED_GRAPHQL = """
|
|
53
|
+
query($login: String!, $cursor: String) {
|
|
54
|
+
organization(login: $login)
|
|
55
|
+
{
|
|
56
|
+
url
|
|
57
|
+
login
|
|
58
|
+
enterpriseOwners(first:100, after: $cursor){
|
|
59
|
+
edges {
|
|
60
|
+
node {
|
|
61
|
+
url
|
|
62
|
+
login
|
|
63
|
+
name
|
|
64
|
+
isSiteAdmin
|
|
65
|
+
email
|
|
66
|
+
company
|
|
67
|
+
}
|
|
68
|
+
organizationRole
|
|
69
|
+
}
|
|
70
|
+
pageInfo{
|
|
71
|
+
endCursor
|
|
72
|
+
hasNextPage
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
"""
|
|
78
|
+
|
|
47
79
|
|
|
48
80
|
@timeit
|
|
49
|
-
def
|
|
81
|
+
def get_users(token: str, api_url: str, organization: str) -> Tuple[List[Dict], Dict]:
|
|
50
82
|
"""
|
|
51
83
|
Retrieve a list of users from the given GitHub organization as described in
|
|
52
84
|
https://docs.github.com/en/graphql/reference/objects#organizationmemberedge.
|
|
53
85
|
:param token: The Github API token as string.
|
|
54
86
|
:param api_url: The Github v4 API endpoint as string.
|
|
55
87
|
:param organization: The name of the target Github organization as string.
|
|
56
|
-
:return: A 2-tuple containing
|
|
57
|
-
|
|
88
|
+
:return: A 2-tuple containing
|
|
89
|
+
1. a list of dicts representing users and
|
|
90
|
+
2. data on the owning GitHub organization
|
|
91
|
+
see tests.data.github.users.GITHUB_USER_DATA for shape of both
|
|
58
92
|
"""
|
|
59
93
|
users, org = fetch_all(
|
|
60
94
|
token,
|
|
@@ -66,56 +100,139 @@ def get(token: str, api_url: str, organization: str) -> Tuple[List[Dict], Dict]:
|
|
|
66
100
|
return users.edges, org
|
|
67
101
|
|
|
68
102
|
|
|
103
|
+
def get_enterprise_owners(token: str, api_url: str, organization: str) -> Tuple[List[Dict], Dict]:
|
|
104
|
+
"""
|
|
105
|
+
Retrieve a list of enterprise owners from the given GitHub organization as described in
|
|
106
|
+
https://docs.github.com/en/graphql/reference/objects#organizationenterpriseowneredge.
|
|
107
|
+
:param token: The Github API token as string.
|
|
108
|
+
:param api_url: The Github v4 API endpoint as string.
|
|
109
|
+
:param organization: The name of the target Github organization as string.
|
|
110
|
+
:return: A 2-tuple containing
|
|
111
|
+
1. a list of dicts representing users who are enterprise owners
|
|
112
|
+
3. data on the owning GitHub organization
|
|
113
|
+
see tests.data.github.users.GITHUB_ENTERPRISE_OWNER_DATA for shape
|
|
114
|
+
"""
|
|
115
|
+
owners, org = fetch_all(
|
|
116
|
+
token,
|
|
117
|
+
api_url,
|
|
118
|
+
organization,
|
|
119
|
+
GITHUB_ENTERPRISE_OWNER_USERS_PAGINATED_GRAPHQL,
|
|
120
|
+
'enterpriseOwners',
|
|
121
|
+
)
|
|
122
|
+
return owners.edges, org
|
|
123
|
+
|
|
124
|
+
|
|
69
125
|
@timeit
|
|
70
|
-
def
|
|
71
|
-
|
|
126
|
+
def transform_users(user_data: List[Dict], owners_data: List[Dict], org_data: Dict) -> Tuple[List[Dict], List[Dict]]:
|
|
127
|
+
"""
|
|
128
|
+
Taking raw user and owner data, return two lists of processed user data:
|
|
129
|
+
* organization users aka affiliated users (users directly affiliated with an organization)
|
|
130
|
+
* unaffiliated users (user who, for example, are enterprise owners but not members of the target organization).
|
|
131
|
+
|
|
132
|
+
:param token: The Github API token as string.
|
|
133
|
+
:param api_url: The Github v4 API endpoint as string.
|
|
134
|
+
:param organization: The name of the target Github organization as string.
|
|
135
|
+
:return: A 2-tuple containing
|
|
136
|
+
1. a list of dicts representing users who are affiliated with the target org
|
|
137
|
+
see tests.data.github.users.GITHUB_USER_DATA for shape
|
|
138
|
+
2. a list of dicts representing users who are not affiliated (e.g. enterprise owners who are not also in
|
|
139
|
+
the target org) — see tests.data.github.users.GITHUB_ENTERPRISE_OWNER_DATA for shape
|
|
140
|
+
3. data on the owning GitHub organization
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
users_dict = {}
|
|
144
|
+
for user in user_data:
|
|
145
|
+
processed_user = deepcopy(user['node'])
|
|
146
|
+
processed_user['role'] = user['role']
|
|
147
|
+
processed_user['hasTwoFactorEnabled'] = user['hasTwoFactorEnabled']
|
|
148
|
+
processed_user['MEMBER_OF'] = org_data['url']
|
|
149
|
+
users_dict[processed_user['url']] = processed_user
|
|
150
|
+
|
|
151
|
+
owners_dict = {}
|
|
152
|
+
for owner in owners_data:
|
|
153
|
+
processed_owner = deepcopy(owner['node'])
|
|
154
|
+
processed_owner['isEnterpriseOwner'] = True
|
|
155
|
+
if owner['organizationRole'] == 'UNAFFILIATED':
|
|
156
|
+
processed_owner['UNAFFILIATED'] = org_data['url']
|
|
157
|
+
else:
|
|
158
|
+
processed_owner['MEMBER_OF'] = org_data['url']
|
|
159
|
+
owners_dict[processed_owner['url']] = processed_owner
|
|
160
|
+
|
|
161
|
+
affiliated_users = [] # users affiliated with the target org
|
|
162
|
+
for url, user in users_dict.items():
|
|
163
|
+
user['isEnterpriseOwner'] = url in owners_dict
|
|
164
|
+
affiliated_users.append(user)
|
|
165
|
+
|
|
166
|
+
unaffiliated_users = [] # users not affiliated with the target org
|
|
167
|
+
for url, owner in owners_dict.items():
|
|
168
|
+
if url not in users_dict:
|
|
169
|
+
unaffiliated_users.append(owner)
|
|
170
|
+
|
|
171
|
+
return affiliated_users, unaffiliated_users
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@timeit
|
|
175
|
+
def load_users(
|
|
176
|
+
neo4j_session: neo4j.Session,
|
|
177
|
+
node_schema: GitHubOrganizationUserSchema | GitHubUnaffiliatedUserSchema,
|
|
178
|
+
user_data: List[Dict],
|
|
179
|
+
org_data: Dict,
|
|
72
180
|
update_tag: int,
|
|
73
181
|
) -> None:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"""
|
|
98
|
-
neo4j_session.run(
|
|
99
|
-
query,
|
|
100
|
-
OrgUrl=org_data['url'],
|
|
101
|
-
OrgLogin=org_data['login'],
|
|
102
|
-
UserData=user_data,
|
|
103
|
-
UpdateTag=update_tag,
|
|
182
|
+
logger.info(f"Loading {len(user_data)} GitHub users to the graph")
|
|
183
|
+
load(
|
|
184
|
+
neo4j_session,
|
|
185
|
+
node_schema,
|
|
186
|
+
user_data,
|
|
187
|
+
lastupdated=update_tag,
|
|
188
|
+
org_url=org_data['url'],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@timeit
|
|
193
|
+
def load_organization(
|
|
194
|
+
neo4j_session: neo4j.Session,
|
|
195
|
+
node_schema: GitHubOrganizationSchema,
|
|
196
|
+
org_data: List[Dict[str, Any]],
|
|
197
|
+
update_tag: int,
|
|
198
|
+
) -> None:
|
|
199
|
+
logger.info(f"Loading {len(org_data)} GitHub organization to the graph")
|
|
200
|
+
load(
|
|
201
|
+
neo4j_session,
|
|
202
|
+
node_schema,
|
|
203
|
+
org_data,
|
|
204
|
+
lastupdated=update_tag,
|
|
104
205
|
)
|
|
105
206
|
|
|
106
207
|
|
|
107
208
|
@timeit
|
|
108
209
|
def sync(
|
|
109
210
|
neo4j_session: neo4j.Session,
|
|
110
|
-
common_job_parameters: Dict
|
|
211
|
+
common_job_parameters: Dict,
|
|
111
212
|
github_api_key: str,
|
|
112
213
|
github_url: str,
|
|
113
214
|
organization: str,
|
|
114
215
|
) -> None:
|
|
115
216
|
logger.info("Syncing GitHub users")
|
|
116
|
-
user_data, org_data =
|
|
117
|
-
|
|
118
|
-
|
|
217
|
+
user_data, org_data = get_users(github_api_key, github_url, organization)
|
|
218
|
+
owners_data, org_data = get_enterprise_owners(github_api_key, github_url, organization)
|
|
219
|
+
processed_affiliated_user_data, processed_unaffiliated_user_data = (
|
|
220
|
+
transform_users(user_data, owners_data, org_data)
|
|
221
|
+
)
|
|
222
|
+
load_organization(
|
|
223
|
+
neo4j_session, GitHubOrganizationSchema(), [org_data],
|
|
224
|
+
common_job_parameters['UPDATE_TAG'],
|
|
225
|
+
)
|
|
226
|
+
load_users(
|
|
227
|
+
neo4j_session, GitHubOrganizationUserSchema(), processed_affiliated_user_data, org_data,
|
|
228
|
+
common_job_parameters['UPDATE_TAG'],
|
|
229
|
+
)
|
|
230
|
+
load_users(
|
|
231
|
+
neo4j_session, GitHubUnaffiliatedUserSchema(), processed_unaffiliated_user_data, org_data,
|
|
232
|
+
common_job_parameters['UPDATE_TAG'],
|
|
233
|
+
)
|
|
234
|
+
# no automated cleanup job for users because user node has no sub_resource_relationship
|
|
235
|
+
run_cleanup_job('github_org_and_users_cleanup.json', neo4j_session, common_job_parameters)
|
|
119
236
|
merge_module_sync_metadata(
|
|
120
237
|
neo4j_session,
|
|
121
238
|
group_type='GitHubOrganization',
|
cartography/intel/okta/users.py
CHANGED
|
@@ -150,7 +150,8 @@ def _load_okta_users(
|
|
|
150
150
|
new_user.okta_last_updated = user_data.okta_last_updated,
|
|
151
151
|
new_user.password_changed = user_data.password_changed,
|
|
152
152
|
new_user.transition_to_status = user_data.transition_to_status,
|
|
153
|
-
new_user.lastupdated = $okta_update_tag
|
|
153
|
+
new_user.lastupdated = $okta_update_tag,
|
|
154
|
+
new_user :UserAccount
|
|
154
155
|
WITH new_user, org
|
|
155
156
|
MERGE (org)-[org_r:RESOURCE]->(new_user)
|
|
156
157
|
ON CREATE SET org_r.firstseen = timestamp()
|
|
@@ -26,5 +26,5 @@ def start_semgrep_ingestion(
|
|
|
26
26
|
# sync_deployment must be called first since it populates common_job_parameters
|
|
27
27
|
# with the deployment ID and slug, which are required by the other sync functions
|
|
28
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)
|
|
29
|
+
sync_dependencies(neo4j_session, config.semgrep_app_token, config.semgrep_dependency_ecosystems, config.update_tag, common_job_parameters) # noqa: E501
|
|
30
30
|
sync_findings(neo4j_session, config.semgrep_app_token, config.update_tag, common_job_parameters)
|
|
@@ -12,6 +12,7 @@ from requests.exceptions import ReadTimeout
|
|
|
12
12
|
from cartography.client.core.tx import load
|
|
13
13
|
from cartography.graph.job import GraphJob
|
|
14
14
|
from cartography.models.semgrep.dependencies import SemgrepGoLibrarySchema
|
|
15
|
+
from cartography.models.semgrep.dependencies import SemgrepNpmLibrarySchema
|
|
15
16
|
from cartography.stats import get_stats_client
|
|
16
17
|
from cartography.util import merge_module_sync_metadata
|
|
17
18
|
from cartography.util import timeit
|
|
@@ -22,16 +23,38 @@ _PAGE_SIZE = 10000
|
|
|
22
23
|
_TIMEOUT = (60, 60)
|
|
23
24
|
_MAX_RETRIES = 3
|
|
24
25
|
|
|
26
|
+
# The keys in this dictionary must be in Semgrep's list of supported ecosystems, defined here:
|
|
27
|
+
# https://semgrep.dev/api/v1/docs/#tag/SupplyChainService/operation/semgrep_app.products.sca.handlers.dependency.list_dependencies_conexxion
|
|
28
|
+
ECOSYSTEM_TO_SCHEMA: Dict = {
|
|
29
|
+
'gomod': SemgrepGoLibrarySchema,
|
|
30
|
+
'npm': SemgrepNpmLibrarySchema,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_and_validate_semgrep_ecosystems(ecosystems: str) -> List[str]:
|
|
35
|
+
validated_ecosystems: List[str] = []
|
|
36
|
+
for ecosystem in ecosystems.split(','):
|
|
37
|
+
ecosystem = ecosystem.strip().lower()
|
|
38
|
+
|
|
39
|
+
if ecosystem in ECOSYSTEM_TO_SCHEMA:
|
|
40
|
+
validated_ecosystems.append(ecosystem)
|
|
41
|
+
else:
|
|
42
|
+
valid_ecosystems: str = ','.join(ECOSYSTEM_TO_SCHEMA.keys())
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f'Error parsing `semgrep-dependency-ecosystems`. You specified "{ecosystems}". '
|
|
45
|
+
f'Please check that your input is formatted as comma-separated values, e.g. "gomod,npm". '
|
|
46
|
+
f'Full list of supported ecosystems: {valid_ecosystems}.',
|
|
47
|
+
)
|
|
48
|
+
return validated_ecosystems
|
|
49
|
+
|
|
25
50
|
|
|
26
51
|
@timeit
|
|
27
|
-
def get_dependencies(semgrep_app_token: str, deployment_id: str,
|
|
52
|
+
def get_dependencies(semgrep_app_token: str, deployment_id: str, ecosystem: str) -> List[Dict[str, Any]]:
|
|
28
53
|
"""
|
|
29
|
-
Gets all dependencies for the given
|
|
54
|
+
Gets all dependencies for the given ecosystem within the given Semgrep deployment ID.
|
|
30
55
|
param: semgrep_app_token: The Semgrep App token to use for authentication.
|
|
31
56
|
param: deployment_id: The Semgrep deployment ID to use for retrieving dependencies.
|
|
32
|
-
param:
|
|
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
|
|
57
|
+
param: ecosystem: The ecosystem to import dependencies from, e.g. "gomod" or "npm".
|
|
35
58
|
"""
|
|
36
59
|
all_deps = []
|
|
37
60
|
deps_url = f"https://semgrep.dev/api/v1/deployments/{deployment_id}/dependencies"
|
|
@@ -46,31 +69,31 @@ def get_dependencies(semgrep_app_token: str, deployment_id: str, ecosystems: Lis
|
|
|
46
69
|
request_data: dict[str, Any] = {
|
|
47
70
|
"pageSize": _PAGE_SIZE,
|
|
48
71
|
"dependencyFilter": {
|
|
49
|
-
"ecosystem":
|
|
72
|
+
"ecosystem": [ecosystem],
|
|
50
73
|
},
|
|
51
74
|
}
|
|
52
75
|
|
|
53
|
-
logger.info(f"Retrieving Semgrep dependencies for deployment '{deployment_id}'.")
|
|
76
|
+
logger.info(f"Retrieving Semgrep {ecosystem} dependencies for deployment '{deployment_id}'.")
|
|
54
77
|
while has_more:
|
|
55
78
|
try:
|
|
56
79
|
response = requests.post(deps_url, json=request_data, headers=headers, timeout=_TIMEOUT)
|
|
57
80
|
response.raise_for_status()
|
|
58
81
|
data = response.json()
|
|
59
82
|
except (ReadTimeout, HTTPError):
|
|
60
|
-
logger.warning(f"Failed to retrieve Semgrep dependencies for page {page}. Retrying...")
|
|
83
|
+
logger.warning(f"Failed to retrieve Semgrep {ecosystem} dependencies for page {page}. Retrying...")
|
|
61
84
|
retries += 1
|
|
62
85
|
if retries >= _MAX_RETRIES:
|
|
63
86
|
raise
|
|
64
87
|
continue
|
|
65
88
|
deps = data.get("dependencies", [])
|
|
66
89
|
has_more = data.get("hasMore", False)
|
|
67
|
-
logger.info(f"Processed page {page} of Semgrep dependencies.")
|
|
90
|
+
logger.info(f"Processed page {page} of Semgrep {ecosystem} dependencies.")
|
|
68
91
|
all_deps.extend(deps)
|
|
69
92
|
retries = 0
|
|
70
93
|
page += 1
|
|
71
94
|
request_data["cursor"] = data.get("cursor")
|
|
72
95
|
|
|
73
|
-
logger.info(f"Retrieved {len(all_deps)} Semgrep dependencies in {page} pages.")
|
|
96
|
+
logger.info(f"Retrieved {len(all_deps)} Semgrep {ecosystem} dependencies in {page} pages.")
|
|
74
97
|
return all_deps
|
|
75
98
|
|
|
76
99
|
|
|
@@ -157,19 +180,18 @@ def load_dependencies(
|
|
|
157
180
|
@timeit
|
|
158
181
|
def cleanup(
|
|
159
182
|
neo4j_session: neo4j.Session,
|
|
183
|
+
dependency_schema: Callable,
|
|
160
184
|
common_job_parameters: Dict[str, Any],
|
|
161
185
|
) -> None:
|
|
162
|
-
logger.info("Running Semgrep
|
|
163
|
-
|
|
164
|
-
SemgrepGoLibrarySchema(), common_job_parameters,
|
|
165
|
-
)
|
|
166
|
-
go_libraries_cleanup_job.run(neo4j_session)
|
|
186
|
+
logger.info(f"Running Semgrep Dependencies cleanup job for {dependency_schema().label}.")
|
|
187
|
+
GraphJob.from_node_schema(dependency_schema(), common_job_parameters).run(neo4j_session)
|
|
167
188
|
|
|
168
189
|
|
|
169
190
|
@timeit
|
|
170
191
|
def sync_dependencies(
|
|
171
192
|
neo4j_session: neo4j.Session,
|
|
172
193
|
semgrep_app_token: str,
|
|
194
|
+
ecosystems_str: str,
|
|
173
195
|
update_tag: int,
|
|
174
196
|
common_job_parameters: Dict[str, Any],
|
|
175
197
|
) -> None:
|
|
@@ -177,19 +199,29 @@ def sync_dependencies(
|
|
|
177
199
|
deployment_id = common_job_parameters.get("DEPLOYMENT_ID")
|
|
178
200
|
if not deployment_id:
|
|
179
201
|
logger.warning(
|
|
180
|
-
"Missing Semgrep deployment ID, ensure that sync_deployment() has been called."
|
|
202
|
+
"Missing Semgrep deployment ID, ensure that sync_deployment() has been called. "
|
|
181
203
|
"Skipping Semgrep dependencies sync job.",
|
|
182
204
|
)
|
|
183
205
|
return
|
|
184
206
|
|
|
185
|
-
|
|
207
|
+
if not ecosystems_str:
|
|
208
|
+
logger.warning(
|
|
209
|
+
"Semgrep is not configured to import dependencies for any ecosystems, see docs to configure. "
|
|
210
|
+
"Skipping Semgrep dependencies sync job.",
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# We don't expect an error here since we've already validated the input in cli.py
|
|
215
|
+
ecosystems = parse_and_validate_semgrep_ecosystems(ecosystems_str)
|
|
186
216
|
|
|
187
|
-
|
|
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)
|
|
217
|
+
logger.info("Running Semgrep dependencies sync job.")
|
|
191
218
|
|
|
192
|
-
|
|
219
|
+
for ecosystem in ecosystems:
|
|
220
|
+
schema = ECOSYSTEM_TO_SCHEMA[ecosystem]
|
|
221
|
+
raw_deps = get_dependencies(semgrep_app_token, deployment_id, ecosystem)
|
|
222
|
+
deps = transform_dependencies(raw_deps)
|
|
223
|
+
load_dependencies(neo4j_session, schema, deps, deployment_id, update_tag)
|
|
224
|
+
cleanup(neo4j_session, schema, common_job_parameters)
|
|
193
225
|
|
|
194
226
|
merge_module_sync_metadata(
|
|
195
227
|
neo4j_session=neo4j_session,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This schema does not handle the org's relationships. Those are handled by other schemas, for example:
|
|
3
|
+
* GitHubTeamSchema defines (GitHubOrganization)-[RESOURCE]->(GitHubTeam)
|
|
4
|
+
* GitHubUserSchema defines (GitHubUser)-[MEMBER_OF|UNAFFILIATED]->(GitHubOrganization)
|
|
5
|
+
(There may be others, these are just two examples.)
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from cartography.models.core.common import PropertyRef
|
|
10
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
11
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class GitHubOrganizationNodeProperties(CartographyNodeProperties):
|
|
16
|
+
id: PropertyRef = PropertyRef('url')
|
|
17
|
+
username: PropertyRef = PropertyRef('login', extra_index=True)
|
|
18
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class GitHubOrganizationSchema(CartographyNodeSchema):
|
|
23
|
+
label: str = 'GitHubOrganization'
|
|
24
|
+
properties: GitHubOrganizationNodeProperties = GitHubOrganizationNodeProperties()
|
|
25
|
+
other_relationships = None
|
|
26
|
+
sub_resource_relationship = None
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RE: Tenant relationship between GitHubUser and GitHubOrganization
|
|
3
|
+
|
|
4
|
+
Note this relationship is implemented via 'other_relationships' and not via the 'sub_resource_relationship'
|
|
5
|
+
as might be expected.
|
|
6
|
+
|
|
7
|
+
The 'sub_resource_relationship' typically describes the relationship of a node to its tenant (the org, project, or
|
|
8
|
+
other resource to which other nodes belong). An assumption of that relationship is that if the tenant goes
|
|
9
|
+
away, all nodes related to it should be cleaned up.
|
|
10
|
+
|
|
11
|
+
In GitHub, though the GitHubUser's tenant seems to be GitHubOrganization, users actually exist independently. There
|
|
12
|
+
is a concept of 'UNAFFILIATED' users (https://docs.github.com/en/graphql/reference/enums#roleinorganization) like
|
|
13
|
+
Enterprise Owners who are related to an org even if they are not direct members of it. You would not want them to be
|
|
14
|
+
cleaned up, if an org goes away, and you could want them in your graph even if they are not members of any org in
|
|
15
|
+
the enterprise.
|
|
16
|
+
|
|
17
|
+
To allow for this in the schema, this relationship is treated as any other node-to-node relationship, via
|
|
18
|
+
'other_relationships', instead of as the typical 'sub_resource_relationship'.
|
|
19
|
+
|
|
20
|
+
RE: GitHubOrganizationUserSchema vs GitHubUnaffiliatedUserSchema
|
|
21
|
+
|
|
22
|
+
As noted above, there are implicitly two types of users, those that are part of, or affiliated, to a target
|
|
23
|
+
GitHubOrganization, and those thare are not part, or unaffiliated. Both are represented as GitHubUser nodes,
|
|
24
|
+
but there are two schemas below to allow for some differences between them, e.g., unaffiliated lack these properties:
|
|
25
|
+
* the 'role' property, because unaffiliated have no 'role' in the target org
|
|
26
|
+
* the 'has_2fa_enabled' property, because the GitHub api does not return it, for these users
|
|
27
|
+
The main importance of having two schemas is to allow the two sets of users to be loaded separately. If we are loading
|
|
28
|
+
an unaffiliated user, but the user already exists in the graph (perhaps they are members of another GitHub orgs for
|
|
29
|
+
example), then loading the unaffiliated user will not blank out the 'role' and 'has_2fa_enabled' properties.
|
|
30
|
+
"""
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
from cartography.models.core.common import PropertyRef
|
|
34
|
+
from cartography.models.core.nodes import CartographyNodeProperties
|
|
35
|
+
from cartography.models.core.nodes import CartographyNodeSchema
|
|
36
|
+
from cartography.models.core.relationships import CartographyRelProperties
|
|
37
|
+
from cartography.models.core.relationships import CartographyRelSchema
|
|
38
|
+
from cartography.models.core.relationships import LinkDirection
|
|
39
|
+
from cartography.models.core.relationships import make_target_node_matcher
|
|
40
|
+
from cartography.models.core.relationships import OtherRelationships
|
|
41
|
+
from cartography.models.core.relationships import TargetNodeMatcher
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class BaseGitHubUserNodeProperties(CartographyNodeProperties):
|
|
46
|
+
# core properties in all GitHubUser nodes
|
|
47
|
+
id: PropertyRef = PropertyRef('url')
|
|
48
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
49
|
+
fullname: PropertyRef = PropertyRef('name')
|
|
50
|
+
username: PropertyRef = PropertyRef('login', extra_index=True)
|
|
51
|
+
is_site_admin: PropertyRef = PropertyRef('isSiteAdmin')
|
|
52
|
+
is_enterprise_owner: PropertyRef = PropertyRef('isEnterpriseOwner')
|
|
53
|
+
email: PropertyRef = PropertyRef('email')
|
|
54
|
+
company: PropertyRef = PropertyRef('company')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class GitHubOrganizationUserNodeProperties(BaseGitHubUserNodeProperties):
|
|
59
|
+
# specified for affiliated users only. The GitHub api does not return this property for unaffiliated users.
|
|
60
|
+
has_2fa_enabled: PropertyRef = PropertyRef('hasTwoFactorEnabled')
|
|
61
|
+
# specified for affiliated uers only. Unaffiliated users do not have a 'role' in the target organization.
|
|
62
|
+
role: PropertyRef = PropertyRef('role')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class GitHubUnaffiliatedUserNodeProperties(BaseGitHubUserNodeProperties):
|
|
67
|
+
# No additional properties needed
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class GitHubUserToOrganizationRelProperties(CartographyRelProperties):
|
|
73
|
+
lastupdated: PropertyRef = PropertyRef('lastupdated', set_in_kwargs=True)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class GitHubUserMemberOfOrganizationRel(CartographyRelSchema):
|
|
78
|
+
target_node_label: str = 'GitHubOrganization'
|
|
79
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
80
|
+
{'id': PropertyRef('MEMBER_OF')},
|
|
81
|
+
)
|
|
82
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
83
|
+
rel_label: str = "MEMBER_OF"
|
|
84
|
+
properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class GitHubUserUnaffiliatedOrganizationRel(CartographyRelSchema):
|
|
89
|
+
target_node_label: str = 'GitHubOrganization'
|
|
90
|
+
target_node_matcher: TargetNodeMatcher = make_target_node_matcher(
|
|
91
|
+
{'id': PropertyRef('UNAFFILIATED')},
|
|
92
|
+
)
|
|
93
|
+
direction: LinkDirection = LinkDirection.OUTWARD
|
|
94
|
+
rel_label: str = "UNAFFILIATED"
|
|
95
|
+
properties: GitHubUserToOrganizationRelProperties = GitHubUserToOrganizationRelProperties()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class GitHubOrganizationUserSchema(CartographyNodeSchema):
|
|
100
|
+
label: str = 'GitHubUser'
|
|
101
|
+
properties: GitHubOrganizationUserNodeProperties = GitHubOrganizationUserNodeProperties()
|
|
102
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
103
|
+
[
|
|
104
|
+
GitHubUserMemberOfOrganizationRel(),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
sub_resource_relationship = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class GitHubUnaffiliatedUserSchema(CartographyNodeSchema):
|
|
112
|
+
label: str = 'GitHubUser'
|
|
113
|
+
properties: GitHubUnaffiliatedUserNodeProperties = GitHubUnaffiliatedUserNodeProperties()
|
|
114
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
115
|
+
[
|
|
116
|
+
GitHubUserUnaffiliatedOrganizationRel(),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
sub_resource_relationship = None
|
|
@@ -75,3 +75,16 @@ class SemgrepGoLibrarySchema(CartographyNodeSchema):
|
|
|
75
75
|
SemgrepDependencyToGithubRepoRel(),
|
|
76
76
|
],
|
|
77
77
|
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class SemgrepNpmLibrarySchema(CartographyNodeSchema):
|
|
82
|
+
label: str = 'NpmLibrary'
|
|
83
|
+
extra_node_labels: Optional[ExtraNodeLabels] = ExtraNodeLabels(['Dependency', 'SemgrepDependency'])
|
|
84
|
+
properties: SemgrepDependencyNodeProperties = SemgrepDependencyNodeProperties()
|
|
85
|
+
sub_resource_relationship: SemgrepDependencyToSemgrepDeploymentSchema = SemgrepDependencyToSemgrepDeploymentSchema()
|
|
86
|
+
other_relationships: OtherRelationships = OtherRelationships(
|
|
87
|
+
[
|
|
88
|
+
SemgrepDependencyToGithubRepoRel(),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cartography
|
|
3
|
-
Version: 0.96.
|
|
3
|
+
Version: 0.96.0rc2
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cartography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
cartography/__main__.py,sha256=JftXT_nUPkqcEh8uxCCT4n-OyHYqbldEgrDS-4ygy0U,101
|
|
3
|
-
cartography/cli.py,sha256=
|
|
4
|
-
cartography/config.py,sha256=
|
|
3
|
+
cartography/cli.py,sha256=LPjeOkx-cKhRkuhqMicB-0X3SHOjLXxEeGqsp2FtpC0,33285
|
|
4
|
+
cartography/config.py,sha256=VpFCSQU88cz6GIfppQcc7LywthpR0lgYnKATyXVws_E,11846
|
|
5
5
|
cartography/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
cartography/stats.py,sha256=dbybb9V2FuvSuHjjNwz6Vjwnd1hap2C7h960rLoKcl8,4406
|
|
7
7
|
cartography/sync.py,sha256=ziD63T_774gXSuD5zdz6fLGvv1Kt2ntQySSVbmcCZb8,9708
|
|
@@ -12,7 +12,7 @@ cartography/client/aws/iam.py,sha256=dYsGikc36DEsSeR2XVOVFFUDwuU9yWj_EVkpgVYCFgM
|
|
|
12
12
|
cartography/client/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
cartography/client/core/tx.py,sha256=4_kTBxrtlwsOM-e8Xtjf7wmmzwZ-DGRJL0rPFp0Xj0Q,10805
|
|
14
14
|
cartography/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
cartography/data/indexes.cypher,sha256=
|
|
15
|
+
cartography/data/indexes.cypher,sha256=zMEzytgcvMLgclMmCkFUdWp4t_EFsOEOp1M2v1vGctM,27208
|
|
16
16
|
cartography/data/permission_relationships.yaml,sha256=RuKGGc_3ZUQ7ag0MssB8k_zaonCkVM5E8I_svBWTmGc,969
|
|
17
17
|
cartography/data/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
cartography/data/jobs/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -45,6 +45,7 @@ cartography/data/jobs/cleanup/aws_import_es_cleanup.json,sha256=VqRqiMcT0Ag0Qif2
|
|
|
45
45
|
cartography/data/jobs/cleanup/aws_import_groups_cleanup.json,sha256=QSdWIpC_Ru4TFBm9I5mpI-ZHyusbzCzCePWN-yn8TQU,463
|
|
46
46
|
cartography/data/jobs/cleanup/aws_import_groups_membership_cleanup.json,sha256=OSeasbv01pn47J1ib3yal_dAzbEOCZVJnldMqK0SAOQ,292
|
|
47
47
|
cartography/data/jobs/cleanup/aws_import_groups_policy_cleanup.json,sha256=U_yKHAsjHoI9fiiTqGyhlMazgeZumlZwMO70CpJpuAM,309
|
|
48
|
+
cartography/data/jobs/cleanup/aws_import_identity_center_cleanup.json,sha256=PSyerOlZkiLaWYFa3Hg1mqqX83FGwj36-p0BF1lR-Q8,650
|
|
48
49
|
cartography/data/jobs/cleanup/aws_import_internet_gateways_cleanup.json,sha256=SHHrR9pX0V1r-RxwJggHGeFA7JVQAAK02D-n-vqOvdU,287
|
|
49
50
|
cartography/data/jobs/cleanup/aws_import_kms_cleanup.json,sha256=NU9doXVk4UmAY59aYnld95Wc1b8DrfCbvJltUsATxko,1441
|
|
50
51
|
cartography/data/jobs/cleanup/aws_import_lambda_cleanup.json,sha256=ntjEpy_WyLxD66Dxydi9GliBVK0DVZWza6908KYeoyA,2268
|
|
@@ -101,8 +102,8 @@ cartography/data/jobs/cleanup/gcp_crm_project_cleanup.json,sha256=JImcuuz9HI2TL0
|
|
|
101
102
|
cartography/data/jobs/cleanup/gcp_dns_cleanup.json,sha256=NGs5UYFmm65Rq8gyqbzIe8_OnFchfpNFf9iAcIj_hyY,1286
|
|
102
103
|
cartography/data/jobs/cleanup/gcp_gke_cluster_cleanup.json,sha256=3bMEJ44AEvjkj_1ibclk6Ys5r1LniUWefpZ_U5hTwHI,671
|
|
103
104
|
cartography/data/jobs/cleanup/gcp_storage_bucket_cleanup.json,sha256=sGygB_meoCpGdGgEZtIlC4L-19meAXdfP99gkNJHD7o,1288
|
|
105
|
+
cartography/data/jobs/cleanup/github_org_and_users_cleanup.json,sha256=vjaOlWdnjaCHmvmaWadOzHXqFnjpR1wW8cykb_M54fM,1010
|
|
104
106
|
cartography/data/jobs/cleanup/github_repos_cleanup.json,sha256=tFXDcsWyCrr5C4Hzl157sv8GjF3aj9dsKKs94bhcEYA,2628
|
|
105
|
-
cartography/data/jobs/cleanup/github_users_cleanup.json,sha256=wxqbOkOShVA3mYUdZwc8UuS4gH5Al8bc7rij6R0cyDQ,806
|
|
106
107
|
cartography/data/jobs/cleanup/gsuite_ingest_groups_cleanup.json,sha256=ddXAUi6aVi2htf5R1bNn6YrC3SjshjLBgWtlzBgZ9Do,961
|
|
107
108
|
cartography/data/jobs/cleanup/gsuite_ingest_users_cleanup.json,sha256=0qMLbVSTyq5F9vt4-TvVa3YAAvZCpPtzF9EwblaTxWg,353
|
|
108
109
|
cartography/data/jobs/cleanup/jamf_import_computers_cleanup.json,sha256=sEF6VSkOcFO210y3VHFO45PDYi5ZePS6xRm3GL9lW7A,248
|
|
@@ -152,6 +153,7 @@ cartography/intel/aws/elasticache.py,sha256=fCI47aDFmIDyE26GiReKYb6XIZUwrzcvsXBQ
|
|
|
152
153
|
cartography/intel/aws/elasticsearch.py,sha256=ZL7MkXF_bXRSoXuDSI1dwGckRLG2zDB8LuAD07vSLnE,8374
|
|
153
154
|
cartography/intel/aws/emr.py,sha256=xhWBVZngxJRFjMEDxwq3G6SgytRGLq0v2a_CeDvByR0,3372
|
|
154
155
|
cartography/intel/aws/iam.py,sha256=eLw0NkBGKzCI_tQ3wmrx3aUibQerrsxKJd3d0RCKcKQ,32374
|
|
156
|
+
cartography/intel/aws/identitycenter.py,sha256=zIWe_JpXPC-kkWu26aFjYtGsClNG_GaQ3bdCeiRkApc,9475
|
|
155
157
|
cartography/intel/aws/inspector.py,sha256=S22ZgRKEnmnBTJ-u0rodqRPB7_LkSIek47NeBxN4XJw,9336
|
|
156
158
|
cartography/intel/aws/kms.py,sha256=bZUzMxAH_DsAcGTJBs08gg2tLKYu-QWjvMvV9C-6v50,11731
|
|
157
159
|
cartography/intel/aws/lambda_function.py,sha256=KKTyn53xpaMI9WvIqxmsOASFwflHt-2_5ow-zUFc2wg,9890
|
|
@@ -160,7 +162,7 @@ cartography/intel/aws/permission_relationships.py,sha256=IarV9gt5BaplZ5TPo_mfypt
|
|
|
160
162
|
cartography/intel/aws/rds.py,sha256=vnlNYmrO2Cc0PNn31CeG2QwYhwjVosbQFE9Ol1vQyLE,25252
|
|
161
163
|
cartography/intel/aws/redshift.py,sha256=KOqiXIllHmtPTeaNGl-cX4srY5pFE6o12j8MQ5-zWpc,6694
|
|
162
164
|
cartography/intel/aws/resourcegroupstaggingapi.py,sha256=aq4kPF6t8QZZoTxdkQVLXH65Di41CDJVM9llJNe6iaY,10278
|
|
163
|
-
cartography/intel/aws/resources.py,sha256=
|
|
165
|
+
cartography/intel/aws/resources.py,sha256=A8Dc3PtCfDyk5a1ZgAoHthhDPS6aWN_kR0PLwnHdC0Q,3370
|
|
164
166
|
cartography/intel/aws/route53.py,sha256=IYqeQud1HuHnf11A7T-Jeif5DWgjpaaU-Jfr2cLUc_o,14099
|
|
165
167
|
cartography/intel/aws/s3.py,sha256=SVxUMtMSkbdjZv5qOSYIbYb8BQa-QTojbHG85-EFWLA,27034
|
|
166
168
|
cartography/intel/aws/secretsmanager.py,sha256=YogwRPT6qZPVg5HrND71zI-nNn60oxoWaW7eUlhuTS0,3304
|
|
@@ -229,7 +231,7 @@ cartography/intel/gcp/storage.py,sha256=oO_ayEhkXlj2Gn7T5MU41ZXiqwRwe6Ud4wzqyRTs
|
|
|
229
231
|
cartography/intel/github/__init__.py,sha256=y876JJGTDJZEOFCDiNCJfcLNxN24pVj4s2N0YmuuoaE,1914
|
|
230
232
|
cartography/intel/github/repos.py,sha256=YPDdBMk6NkZjwPcqPW5LlCy_OS9tKcrZD6ygiUG93J0,21766
|
|
231
233
|
cartography/intel/github/teams.py,sha256=aXI-XbxlA1IDaAUX0XSdEt6pA2n4ew5j_doj1iNYCDM,6618
|
|
232
|
-
cartography/intel/github/users.py,sha256=
|
|
234
|
+
cartography/intel/github/users.py,sha256=zkMLVNfEMZIYYtPfqTsMXs2BNFzK8SfFRO7qnnlHy_s,8399
|
|
233
235
|
cartography/intel/github/util.py,sha256=K6hbxypy4luKhIE1Uh5VWZc9OyjMK2OuO00vBAQfloA,8049
|
|
234
236
|
cartography/intel/gsuite/__init__.py,sha256=AGIUskGlLCVGHbnQicNpNWi9AvmV7_7hUKTt-hsB2J8,4306
|
|
235
237
|
cartography/intel/gsuite/api.py,sha256=J0dkNdfBVMrEv8vvStQu7YKVxXSyV45WueFhUS4aOG4,10310
|
|
@@ -259,7 +261,7 @@ cartography/intel/okta/organization.py,sha256=YLQc7ETdtf8Vc-CRCYivV_xmVl2Oz0Px53
|
|
|
259
261
|
cartography/intel/okta/origins.py,sha256=LNswsOXx8oBk9tL6Qc_3EuqXpa9qgD6fWEWrbsP-_fE,3730
|
|
260
262
|
cartography/intel/okta/roles.py,sha256=_H3o8RveEBt5Mx_nHle81pYEB3NrGq2R2UGL_ChArFs,5840
|
|
261
263
|
cartography/intel/okta/sync_state.py,sha256=GYtAJlbEObiZKPihcIzVzB6APdI_TwUYzhycsDJ8cDE,702
|
|
262
|
-
cartography/intel/okta/users.py,sha256=
|
|
264
|
+
cartography/intel/okta/users.py,sha256=rkOvLaZkBOV2QrgsCa-O0p2Iq7AfguUO078w7XB8CMM,6422
|
|
263
265
|
cartography/intel/okta/utils.py,sha256=XSXw8QiaaXwL_PfOlbpvjgPfy2Etmt_PkS7T6ruTFmc,2165
|
|
264
266
|
cartography/intel/pagerduty/__init__.py,sha256=kXYtgUFLl4eldveyBoxNiebcfOOktTVvLf6rVkriNuA,2164
|
|
265
267
|
cartography/intel/pagerduty/escalation_policies.py,sha256=5Fnm0WLF9TP1GH0BJ2sZlO6U9tH1KdhCvoMxGtL_318,6455
|
|
@@ -268,8 +270,8 @@ cartography/intel/pagerduty/services.py,sha256=Cjm37mWmuBNXSY49-xUQ3xV0DZ391GTLv
|
|
|
268
270
|
cartography/intel/pagerduty/teams.py,sha256=aRubUXgEVVReyLrXAX_be1E_QBJv3Qlr4n779Jkkz8Q,2498
|
|
269
271
|
cartography/intel/pagerduty/users.py,sha256=oltGssxrnzYsV6QTGP1SsPoA1rCUDStj6vGlGWY695g,1623
|
|
270
272
|
cartography/intel/pagerduty/vendors.py,sha256=WlDHExrWRBegDQKtxBV5nJiYgwoTLxNee4HrQDJ-Pdg,1559
|
|
271
|
-
cartography/intel/semgrep/__init__.py,sha256=
|
|
272
|
-
cartography/intel/semgrep/dependencies.py,sha256=
|
|
273
|
+
cartography/intel/semgrep/__init__.py,sha256=dNosNlFJtIJI-XZMF_5qV2nZ-barQNEZaIkEL-JFTyk,1202
|
|
274
|
+
cartography/intel/semgrep/dependencies.py,sha256=PeEeroI38BUH-TefJeimzxNP5y429y6i-jJj3M2s9N4,8163
|
|
273
275
|
cartography/intel/semgrep/deployment.py,sha256=sh-yJHtdgZjxIJimNnA-DxsM-MYgMhQ_WX2E7w4PWiM,2012
|
|
274
276
|
cartography/intel/semgrep/findings.py,sha256=GDqZmfl9-HiZ93u0jhlkZ1w_qzrD1cFFFJtVc7DFdPk,9761
|
|
275
277
|
cartography/intel/snipeit/__init__.py,sha256=0uIh8NbuI7IbfgaOrPHg4Nfm1yO6mTRC_qaFiIjR2FA,992
|
|
@@ -327,7 +329,9 @@ cartography/models/duo/token.py,sha256=BS_AvF-TAGzCY9Owtqxr8g_s6716dnzFOO1Iwkckm
|
|
|
327
329
|
cartography/models/duo/user.py,sha256=ih3DH_QveAve4cX9dmIwC5gVN6_RNnuLK3bfJ5I9u6g,6554
|
|
328
330
|
cartography/models/duo/web_authn_credential.py,sha256=OcZnfG5zCMlphxSltRcAXQ12hHYJjxrBt6A9L28g7Vk,2920
|
|
329
331
|
cartography/models/github/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
332
|
+
cartography/models/github/orgs.py,sha256=f5kJ-51MDGW5k4sWMeTfyBDxcHdhFJZGkRUvGcjllBU,1097
|
|
330
333
|
cartography/models/github/teams.py,sha256=mk3OFGTDqWkLz7aX7Q9AtpOMOkZDDGH0MWoVeevK2-k,4376
|
|
334
|
+
cartography/models/github/users.py,sha256=XliVsBKWF7wW7Dwjwdoz9E2gk1cASyfQgAddKYr23VY,5739
|
|
331
335
|
cartography/models/kandji/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
332
336
|
cartography/models/kandji/device.py,sha256=C3zPhLi1oPNysbSUr4H2u8b-Xy14sb3FE7YcjCwlntw,2214
|
|
333
337
|
cartography/models/kandji/tenant.py,sha256=KhcbahNBemny3coQPiadIY8B-yDMg_ejYB2BR6vqBfw,674
|
|
@@ -335,7 +339,7 @@ cartography/models/lastpass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
|
335
339
|
cartography/models/lastpass/tenant.py,sha256=TG-9LFo9Sfzb9UgcTt_gFVTKocLItbgQMMPkN_iprXU,618
|
|
336
340
|
cartography/models/lastpass/user.py,sha256=SMTTYN6jgccc9k76hY3rVImElJOhHhZ9f1aZ6JzcrHw,3487
|
|
337
341
|
cartography/models/semgrep/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
338
|
-
cartography/models/semgrep/dependencies.py,sha256=
|
|
342
|
+
cartography/models/semgrep/dependencies.py,sha256=zO8NxfFIvL6vzFWaIDAXqPF7VfUwLMj5YizqA0IS-eA,3959
|
|
339
343
|
cartography/models/semgrep/deployment.py,sha256=or5qZDuR51MXzINpH15jZrqmSUvXQevCNYWJ7D6v-JI,745
|
|
340
344
|
cartography/models/semgrep/findings.py,sha256=RPd-QzvP38fbTIqFARx6XpcZSsd5JM3KIg-ZlJA7NlE,5490
|
|
341
345
|
cartography/models/semgrep/locations.py,sha256=kSk7Nn5Mn4Ob84MVZOo2GR0YFi-9Okq9pgA3FfC6_bk,3061
|
|
@@ -343,9 +347,9 @@ cartography/models/snipeit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
|
343
347
|
cartography/models/snipeit/asset.py,sha256=FyRAaeXuZjMy0eUQcSDFcgEAF5lbLMlvqp1Tv9d3Lv4,3238
|
|
344
348
|
cartography/models/snipeit/tenant.py,sha256=p4rFnpNNuF1W5ilGBbexDaETWTwavfb38RcQGoImkQI,679
|
|
345
349
|
cartography/models/snipeit/user.py,sha256=MsB4MiCVNTH6JpESime7cOkB89autZOXQpL6Z0l7L6o,2113
|
|
346
|
-
cartography-0.96.
|
|
347
|
-
cartography-0.96.
|
|
348
|
-
cartography-0.96.
|
|
349
|
-
cartography-0.96.
|
|
350
|
-
cartography-0.96.
|
|
351
|
-
cartography-0.96.
|
|
350
|
+
cartography-0.96.0rc2.dist-info/LICENSE,sha256=kvLEBRYaQ1RvUni6y7Ti9uHeooqnjPoo6n_-0JO1ETc,11351
|
|
351
|
+
cartography-0.96.0rc2.dist-info/METADATA,sha256=TZRI_txSoyOf6cEy50VjF6X9jA-tjukRutReucOcbBY,1966
|
|
352
|
+
cartography-0.96.0rc2.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
353
|
+
cartography-0.96.0rc2.dist-info/entry_points.txt,sha256=GVIAWD0o0_K077qMA_k1oZU4v-M0a8GLKGJR8tZ-qH8,112
|
|
354
|
+
cartography-0.96.0rc2.dist-info/top_level.txt,sha256=BHqsNJQiI6Q72DeypC1IINQJE59SLhU4nllbQjgJi9g,12
|
|
355
|
+
cartography-0.96.0rc2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|