qontract-reconcile 0.10.1rc505__py3-none-any.whl → 0.10.1rc507__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.
- {qontract_reconcile-0.10.1rc505.dist-info → qontract_reconcile-0.10.1rc507.dist-info}/METADATA +1 -1
- {qontract_reconcile-0.10.1rc505.dist-info → qontract_reconcile-0.10.1rc507.dist-info}/RECORD +12 -10
- reconcile/acs_policies.py +232 -0
- reconcile/acs_rbac.py +12 -33
- reconcile/cli.py +11 -0
- reconcile/gql_definitions/acs/acs_policies.py +159 -0
- reconcile/test/test_acs_policies.py +444 -0
- reconcile/test/test_acs_rbac.py +51 -51
- reconcile/utils/terrascript/cloudflare_resources.py +2 -1
- reconcile/utils/acs_api.py +0 -326
- {qontract_reconcile-0.10.1rc505.dist-info → qontract_reconcile-0.10.1rc507.dist-info}/WHEEL +0 -0
- {qontract_reconcile-0.10.1rc505.dist-info → qontract_reconcile-0.10.1rc507.dist-info}/entry_points.txt +0 -0
- {qontract_reconcile-0.10.1rc505.dist-info → qontract_reconcile-0.10.1rc507.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,444 @@
|
|
1
|
+
import copy
|
2
|
+
from typing import Any
|
3
|
+
from unittest.mock import Mock
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
from pytest_mock import MockerFixture
|
7
|
+
|
8
|
+
from reconcile.acs_policies import AcsPoliciesIntegration
|
9
|
+
from reconcile.gql_definitions.acs.acs_policies import (
|
10
|
+
AcsPolicyConditionsCveV1,
|
11
|
+
AcsPolicyConditionsCvssV1,
|
12
|
+
AcsPolicyConditionsSeverityV1,
|
13
|
+
AcsPolicyQueryData,
|
14
|
+
AcsPolicyScopeClusterV1,
|
15
|
+
AcsPolicyScopeNamespaceV1,
|
16
|
+
AcsPolicyV1,
|
17
|
+
ClusterV1,
|
18
|
+
NamespaceV1,
|
19
|
+
NamespaceV1_ClusterV1,
|
20
|
+
)
|
21
|
+
from reconcile.utils.acs.policies import AcsPolicyApi, Policy, PolicyCondition, Scope
|
22
|
+
|
23
|
+
CUSTOM_POLICY_ONE_NAME = "app-sre-clusters-fixable-cve-7-fixable"
|
24
|
+
CUSTOM_POLICY_ONE_ID = "365d4e71-3241-4448-9f3d-eb0eed1c1820"
|
25
|
+
CUSTOM_POLICY_TWO_NAME = "app-sre-namespaces-severity-critical"
|
26
|
+
CUSTOM_POLICY_TWO_ID = "2200245e-b700-46c2-8793-3e437fca6aa0"
|
27
|
+
JIRA_NOTIFIER_NAME = "app-sre-jira"
|
28
|
+
JIRA_NOTIFIER_ID = "54170627-da34-40cb-839a-af9fbeac10fb"
|
29
|
+
|
30
|
+
|
31
|
+
@pytest.fixture
|
32
|
+
def query_data_desired_state() -> AcsPolicyQueryData:
|
33
|
+
return AcsPolicyQueryData(
|
34
|
+
acs_policies=[
|
35
|
+
AcsPolicyV1(
|
36
|
+
name=CUSTOM_POLICY_ONE_NAME,
|
37
|
+
description="CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
38
|
+
severity="high",
|
39
|
+
notifiers=[JIRA_NOTIFIER_NAME],
|
40
|
+
categories=["vulnerability-management"],
|
41
|
+
scope=AcsPolicyScopeClusterV1(
|
42
|
+
level="cluster",
|
43
|
+
clusters=[
|
44
|
+
ClusterV1(name="app-sre-stage"),
|
45
|
+
ClusterV1(name="app-sre-prod"),
|
46
|
+
],
|
47
|
+
),
|
48
|
+
conditions=[
|
49
|
+
AcsPolicyConditionsCvssV1(
|
50
|
+
policyField="cvss", comparison="gte", score=7
|
51
|
+
),
|
52
|
+
AcsPolicyConditionsCveV1(policyField="cve", fixable=True),
|
53
|
+
],
|
54
|
+
),
|
55
|
+
AcsPolicyV1(
|
56
|
+
name=CUSTOM_POLICY_TWO_NAME,
|
57
|
+
description="image security policy violations of critical severity within app-sre namespaces",
|
58
|
+
severity="critical",
|
59
|
+
notifiers=[],
|
60
|
+
categories=["vulnerability-management", "devops-best-practices"],
|
61
|
+
scope=AcsPolicyScopeNamespaceV1(
|
62
|
+
level="namespace",
|
63
|
+
namespaces=[
|
64
|
+
NamespaceV1(
|
65
|
+
name="app-interface-stage",
|
66
|
+
cluster=NamespaceV1_ClusterV1(name="app-sre-stage"),
|
67
|
+
),
|
68
|
+
NamespaceV1(
|
69
|
+
name="app-interface-production",
|
70
|
+
cluster=NamespaceV1_ClusterV1(name="app-sre-prod"),
|
71
|
+
),
|
72
|
+
],
|
73
|
+
),
|
74
|
+
conditions=[
|
75
|
+
AcsPolicyConditionsSeverityV1(
|
76
|
+
policyField="severity", comparison="eq", level="critical"
|
77
|
+
)
|
78
|
+
],
|
79
|
+
),
|
80
|
+
]
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
@pytest.fixture
|
85
|
+
def modeled_acs_policies() -> list[Policy]:
|
86
|
+
return [
|
87
|
+
Policy(
|
88
|
+
name=CUSTOM_POLICY_ONE_NAME,
|
89
|
+
description="CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
90
|
+
severity="HIGH_SEVERITY",
|
91
|
+
notifiers=[JIRA_NOTIFIER_ID],
|
92
|
+
categories=["Vulnerability Management"],
|
93
|
+
scope=[
|
94
|
+
Scope(cluster="app-sre-prod", namespace=""),
|
95
|
+
Scope(cluster="app-sre-stage", namespace=""),
|
96
|
+
],
|
97
|
+
conditions=[
|
98
|
+
PolicyCondition(field_name="CVSS", values=[">=7"], negate=False),
|
99
|
+
PolicyCondition(field_name="Fixable", values=["true"], negate=False),
|
100
|
+
],
|
101
|
+
),
|
102
|
+
Policy(
|
103
|
+
name=CUSTOM_POLICY_TWO_NAME,
|
104
|
+
description="image security policy violations of critical severity within app-sre namespaces",
|
105
|
+
severity="CRITICAL_SEVERITY",
|
106
|
+
notifiers=[],
|
107
|
+
categories=["DevOps Best Practices", "Vulnerability Management"],
|
108
|
+
scope=[
|
109
|
+
Scope(cluster="app-sre-prod", namespace="app-interface-production"),
|
110
|
+
Scope(cluster="app-sre-stage", namespace="app-interface-stage"),
|
111
|
+
],
|
112
|
+
conditions=[
|
113
|
+
PolicyCondition(
|
114
|
+
field_name="Severity", values=["CRITICAL"], negate=False
|
115
|
+
)
|
116
|
+
],
|
117
|
+
),
|
118
|
+
]
|
119
|
+
|
120
|
+
|
121
|
+
@pytest.fixture
|
122
|
+
def api_response_policies_summary() -> Any:
|
123
|
+
return {
|
124
|
+
"policies": [
|
125
|
+
{
|
126
|
+
"id": CUSTOM_POLICY_ONE_ID,
|
127
|
+
"name": CUSTOM_POLICY_ONE_NAME,
|
128
|
+
"description": "CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
129
|
+
"severity": "HIGH_SEVERITY",
|
130
|
+
"notifiers": [JIRA_NOTIFIER_ID],
|
131
|
+
"disabled": False,
|
132
|
+
"lifecycleStages": ["BUILD"],
|
133
|
+
"lastUpdated": None,
|
134
|
+
"eventSource": "NOT_APPLICABLE",
|
135
|
+
"isDefault": False,
|
136
|
+
},
|
137
|
+
{
|
138
|
+
"id": CUSTOM_POLICY_TWO_ID,
|
139
|
+
"name": CUSTOM_POLICY_TWO_NAME,
|
140
|
+
"description": "image security policy violations of critical severity within app-sre namespaces",
|
141
|
+
"severity": "CRITICAL_SEVERITY",
|
142
|
+
"disabled": False,
|
143
|
+
"lifecycleStages": ["BUILD"],
|
144
|
+
"notifiers": [],
|
145
|
+
"lastUpdated": None,
|
146
|
+
"eventSource": "NOT_APPLICABLE",
|
147
|
+
"isDefault": False,
|
148
|
+
},
|
149
|
+
{
|
150
|
+
"id": "1111245e-7700-46c2-8793-3e437fca6aa0",
|
151
|
+
"name": "some-default-policy",
|
152
|
+
"description": "default policy that should not be included in reconcile",
|
153
|
+
"severity": "CRITICAL_SEVERITY",
|
154
|
+
"disabled": False,
|
155
|
+
"lifecycleStages": ["BUILD"],
|
156
|
+
"notifiers": [],
|
157
|
+
"lastUpdated": None,
|
158
|
+
"eventSource": "NOT_APPLICABLE",
|
159
|
+
"isDefault": True,
|
160
|
+
},
|
161
|
+
]
|
162
|
+
}
|
163
|
+
|
164
|
+
|
165
|
+
@pytest.fixture
|
166
|
+
def api_response_policies_specific() -> list[Any]:
|
167
|
+
return [
|
168
|
+
{
|
169
|
+
"id": CUSTOM_POLICY_ONE_ID,
|
170
|
+
"name": CUSTOM_POLICY_ONE_NAME,
|
171
|
+
"description": "CVEs within app-sre clusters with CVSS score gte to 7 and fixable",
|
172
|
+
"disabled": False,
|
173
|
+
"categories": ["Vulnerability Management"],
|
174
|
+
"lifecycleStages": ["BUILD"],
|
175
|
+
"eventSource": "NOT_APPLICABLE",
|
176
|
+
"exclusions": [],
|
177
|
+
"scope": [
|
178
|
+
{"cluster": "app-sre-stage", "namespace": "", "label": None},
|
179
|
+
{"cluster": "app-sre-prod", "namespace": "", "label": None},
|
180
|
+
],
|
181
|
+
"severity": "HIGH_SEVERITY",
|
182
|
+
"enforcementActions": [],
|
183
|
+
"notifiers": [JIRA_NOTIFIER_ID],
|
184
|
+
"policySections": [
|
185
|
+
{
|
186
|
+
"sectionName": "primary",
|
187
|
+
"policyGroups": [
|
188
|
+
{
|
189
|
+
"fieldName": "CVSS",
|
190
|
+
"booleanOperator": "OR",
|
191
|
+
"negate": False,
|
192
|
+
"values": [{"value": ">=7"}],
|
193
|
+
},
|
194
|
+
{
|
195
|
+
"fieldName": "Fixable",
|
196
|
+
"booleanOperator": "OR",
|
197
|
+
"negate": False,
|
198
|
+
"values": [{"value": "true"}],
|
199
|
+
},
|
200
|
+
],
|
201
|
+
}
|
202
|
+
],
|
203
|
+
"mitreAttackVectors": [],
|
204
|
+
"criteriaLocked": False,
|
205
|
+
"mitreVectorsLocked": False,
|
206
|
+
"isDefault": False,
|
207
|
+
},
|
208
|
+
{
|
209
|
+
"id": CUSTOM_POLICY_TWO_ID,
|
210
|
+
"name": CUSTOM_POLICY_TWO_NAME,
|
211
|
+
"description": "image security policy violations of critical severity within app-sre namespaces",
|
212
|
+
"disabled": False,
|
213
|
+
"categories": ["Vulnerability Management", "DevOps Best Practices"],
|
214
|
+
"lifecycleStages": ["BUILD"],
|
215
|
+
"eventSource": "NOT_APPLICABLE",
|
216
|
+
"exclusions": [],
|
217
|
+
"scope": [
|
218
|
+
{
|
219
|
+
"cluster": "app-sre-stage",
|
220
|
+
"namespace": "app-interface-stage",
|
221
|
+
"label": None,
|
222
|
+
},
|
223
|
+
{
|
224
|
+
"cluster": "app-sre-prod",
|
225
|
+
"namespace": "app-interface-production",
|
226
|
+
"label": None,
|
227
|
+
},
|
228
|
+
],
|
229
|
+
"severity": "CRITICAL_SEVERITY",
|
230
|
+
"enforcementActions": [],
|
231
|
+
"notifiers": [],
|
232
|
+
"policySections": [
|
233
|
+
{
|
234
|
+
"sectionName": "primary",
|
235
|
+
"policyGroups": [
|
236
|
+
{
|
237
|
+
"fieldName": "Severity",
|
238
|
+
"booleanOperator": "OR",
|
239
|
+
"negate": False,
|
240
|
+
"values": [{"value": "CRITICAL"}],
|
241
|
+
}
|
242
|
+
],
|
243
|
+
}
|
244
|
+
],
|
245
|
+
"mitreAttackVectors": [],
|
246
|
+
"criteriaLocked": False,
|
247
|
+
"mitreVectorsLocked": False,
|
248
|
+
"isDefault": False,
|
249
|
+
},
|
250
|
+
]
|
251
|
+
|
252
|
+
|
253
|
+
@pytest.fixture
|
254
|
+
def api_response_list_notifiers() -> list[AcsPolicyApi.NotifierIdentifiers]:
|
255
|
+
return [
|
256
|
+
AcsPolicyApi.NotifierIdentifiers(id=JIRA_NOTIFIER_ID, name=JIRA_NOTIFIER_NAME)
|
257
|
+
]
|
258
|
+
|
259
|
+
|
260
|
+
def test_get_desired_state(
|
261
|
+
mocker: MockerFixture,
|
262
|
+
query_data_desired_state: AcsPolicyQueryData,
|
263
|
+
modeled_acs_policies: list[Policy],
|
264
|
+
api_response_list_notifiers: list[AcsPolicyApi.NotifierIdentifiers],
|
265
|
+
) -> None:
|
266
|
+
query_func = mocker.patch(
|
267
|
+
"reconcile.gql_definitions.acs.acs_policies.query", autospec=True
|
268
|
+
)
|
269
|
+
query_func.return_value = query_data_desired_state
|
270
|
+
|
271
|
+
integration = AcsPoliciesIntegration()
|
272
|
+
result = integration.get_desired_state(
|
273
|
+
query_func=query_func, notifiers=api_response_list_notifiers
|
274
|
+
)
|
275
|
+
assert result == modeled_acs_policies
|
276
|
+
|
277
|
+
|
278
|
+
def test_get_current_state(
|
279
|
+
mocker: MockerFixture,
|
280
|
+
modeled_acs_policies: list[Policy],
|
281
|
+
api_response_policies_summary: list[Any],
|
282
|
+
api_response_policies_specific: list[Any],
|
283
|
+
) -> None:
|
284
|
+
list_custom_policies = Mock()
|
285
|
+
list_custom_policies.json.return_value = api_response_policies_summary
|
286
|
+
specific_custom_policy_1 = Mock()
|
287
|
+
specific_custom_policy_1.json.return_value = api_response_policies_specific[0]
|
288
|
+
specific_custom_policy_2 = Mock()
|
289
|
+
specific_custom_policy_2.json.return_value = api_response_policies_specific[1]
|
290
|
+
mocker.patch.object(
|
291
|
+
AcsPolicyApi,
|
292
|
+
"generic_request",
|
293
|
+
side_effect=[
|
294
|
+
list_custom_policies,
|
295
|
+
specific_custom_policy_1,
|
296
|
+
specific_custom_policy_2,
|
297
|
+
],
|
298
|
+
)
|
299
|
+
with AcsPolicyApi(instance={"url": "foo", "token": "bar"}) as acs:
|
300
|
+
assert sorted(acs.get_custom_policies(), key=lambda p: p.name) == sorted(
|
301
|
+
modeled_acs_policies, key=lambda p: p.name
|
302
|
+
)
|
303
|
+
|
304
|
+
|
305
|
+
def test_create_policy(
|
306
|
+
mocker: MockerFixture, modeled_acs_policies: list[Policy]
|
307
|
+
) -> None:
|
308
|
+
dry_run = False
|
309
|
+
desired = modeled_acs_policies
|
310
|
+
current = modeled_acs_policies[:-1]
|
311
|
+
|
312
|
+
acs_mock = Mock()
|
313
|
+
mocker.patch.object(acs_mock, "create_or_update_policy")
|
314
|
+
|
315
|
+
integration = AcsPoliciesIntegration()
|
316
|
+
integration.reconcile(
|
317
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
318
|
+
)
|
319
|
+
|
320
|
+
acs_mock.create_or_update_policy.assert_has_calls([
|
321
|
+
mocker.call(desired=modeled_acs_policies[1])
|
322
|
+
])
|
323
|
+
|
324
|
+
|
325
|
+
def test_create_policy_dry_run(
|
326
|
+
mocker: MockerFixture, modeled_acs_policies: list[Policy]
|
327
|
+
) -> None:
|
328
|
+
dry_run = True
|
329
|
+
desired = modeled_acs_policies
|
330
|
+
current = modeled_acs_policies[:-1]
|
331
|
+
|
332
|
+
acs_mock = Mock()
|
333
|
+
mocker.patch.object(acs_mock, "create_or_update_policy")
|
334
|
+
|
335
|
+
integration = AcsPoliciesIntegration()
|
336
|
+
integration.reconcile(
|
337
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
338
|
+
)
|
339
|
+
|
340
|
+
acs_mock.create_or_update_policy.assert_not_called()
|
341
|
+
|
342
|
+
|
343
|
+
def test_delete_policy(
|
344
|
+
mocker: MockerFixture,
|
345
|
+
modeled_acs_policies: list[Policy],
|
346
|
+
api_response_policies_summary: Any,
|
347
|
+
) -> None:
|
348
|
+
dry_run = False
|
349
|
+
desired = modeled_acs_policies[:-1]
|
350
|
+
current = modeled_acs_policies
|
351
|
+
|
352
|
+
acs_mock = Mock()
|
353
|
+
mocker.patch.object(acs_mock, "delete_policy")
|
354
|
+
mocker.patch.object(
|
355
|
+
acs_mock,
|
356
|
+
"list_custom_policies",
|
357
|
+
return_value=api_response_policies_summary["policies"][:-1],
|
358
|
+
)
|
359
|
+
|
360
|
+
integration = AcsPoliciesIntegration()
|
361
|
+
integration.reconcile(
|
362
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
363
|
+
)
|
364
|
+
|
365
|
+
acs_mock.delete_policy.assert_has_calls([mocker.call(CUSTOM_POLICY_TWO_ID)])
|
366
|
+
|
367
|
+
|
368
|
+
def test_delete_policy_dry_run(
|
369
|
+
mocker: MockerFixture,
|
370
|
+
modeled_acs_policies: list[Policy],
|
371
|
+
api_response_policies_summary: Any,
|
372
|
+
) -> None:
|
373
|
+
dry_run = True
|
374
|
+
desired = modeled_acs_policies[:-1]
|
375
|
+
current = modeled_acs_policies
|
376
|
+
|
377
|
+
acs_mock = Mock()
|
378
|
+
mocker.patch.object(acs_mock, "delete_policy")
|
379
|
+
mocker.patch.object(
|
380
|
+
acs_mock,
|
381
|
+
"list_custom_policies",
|
382
|
+
return_value=api_response_policies_summary["policies"][:-1],
|
383
|
+
)
|
384
|
+
|
385
|
+
integration = AcsPoliciesIntegration()
|
386
|
+
integration.reconcile(
|
387
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
388
|
+
)
|
389
|
+
|
390
|
+
acs_mock.delete_policy.assert_not_called()
|
391
|
+
|
392
|
+
|
393
|
+
def test_update_policy(
|
394
|
+
mocker: MockerFixture,
|
395
|
+
modeled_acs_policies: list[Policy],
|
396
|
+
api_response_policies_summary: Any,
|
397
|
+
) -> None:
|
398
|
+
dry_run = False
|
399
|
+
desired = modeled_acs_policies
|
400
|
+
current = copy.deepcopy(modeled_acs_policies)
|
401
|
+
current[0].severity = "LOW_SEVERITY"
|
402
|
+
|
403
|
+
acs_mock = Mock()
|
404
|
+
mocker.patch.object(acs_mock, "create_or_update_policy")
|
405
|
+
mocker.patch.object(
|
406
|
+
acs_mock,
|
407
|
+
"list_custom_policies",
|
408
|
+
return_value=api_response_policies_summary["policies"],
|
409
|
+
)
|
410
|
+
|
411
|
+
integration = AcsPoliciesIntegration()
|
412
|
+
integration.reconcile(
|
413
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
414
|
+
)
|
415
|
+
|
416
|
+
acs_mock.create_or_update_policy.assert_has_calls([
|
417
|
+
mocker.call(desired=desired[0], id=CUSTOM_POLICY_ONE_ID)
|
418
|
+
])
|
419
|
+
|
420
|
+
|
421
|
+
def test_update_policy_dry_run(
|
422
|
+
mocker: MockerFixture,
|
423
|
+
modeled_acs_policies: list[Policy],
|
424
|
+
api_response_policies_summary: Any,
|
425
|
+
) -> None:
|
426
|
+
dry_run = True
|
427
|
+
desired = modeled_acs_policies
|
428
|
+
current = copy.deepcopy(modeled_acs_policies)
|
429
|
+
current[0].severity = "LOW_SEVERITY"
|
430
|
+
|
431
|
+
acs_mock = Mock()
|
432
|
+
mocker.patch.object(acs_mock, "create_or_update_policy")
|
433
|
+
mocker.patch.object(
|
434
|
+
acs_mock,
|
435
|
+
"list_custom_policies",
|
436
|
+
return_value=api_response_policies_summary["policies"],
|
437
|
+
)
|
438
|
+
|
439
|
+
integration = AcsPoliciesIntegration()
|
440
|
+
integration.reconcile(
|
441
|
+
desired=desired, current=current, acs=acs_mock, dry_run=dry_run
|
442
|
+
)
|
443
|
+
|
444
|
+
acs_mock.create_or_update_policy.assert_not_called()
|