qontract-reconcile 0.10.1rc506__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.
@@ -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()