regscale-cli 6.23.0.0__py3-none-any.whl → 6.24.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

Files changed (44) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +2 -0
  3. regscale/integrations/commercial/__init__.py +1 -0
  4. regscale/integrations/commercial/sarif/sarif_converter.py +1 -1
  5. regscale/integrations/commercial/wizv2/click.py +109 -2
  6. regscale/integrations/commercial/wizv2/compliance_report.py +1485 -0
  7. regscale/integrations/commercial/wizv2/constants.py +72 -2
  8. regscale/integrations/commercial/wizv2/data_fetcher.py +61 -0
  9. regscale/integrations/commercial/wizv2/file_cleanup.py +104 -0
  10. regscale/integrations/commercial/wizv2/issue.py +775 -27
  11. regscale/integrations/commercial/wizv2/policy_compliance.py +599 -181
  12. regscale/integrations/commercial/wizv2/reports.py +243 -0
  13. regscale/integrations/commercial/wizv2/scanner.py +668 -245
  14. regscale/integrations/compliance_integration.py +304 -51
  15. regscale/integrations/due_date_handler.py +210 -0
  16. regscale/integrations/public/cci_importer.py +444 -0
  17. regscale/integrations/scanner_integration.py +718 -153
  18. regscale/models/integration_models/CCI_List.xml +1 -0
  19. regscale/models/integration_models/cisa_kev_data.json +61 -3
  20. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  21. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +3 -3
  22. regscale/models/regscale_models/form_field_value.py +1 -1
  23. regscale/models/regscale_models/milestone.py +1 -0
  24. regscale/models/regscale_models/regscale_model.py +225 -60
  25. regscale/models/regscale_models/security_plan.py +3 -2
  26. regscale/regscale.py +7 -0
  27. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/METADATA +9 -9
  28. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/RECORD +44 -27
  29. tests/fixtures/test_fixture.py +13 -8
  30. tests/regscale/integrations/public/__init__.py +0 -0
  31. tests/regscale/integrations/public/test_alienvault.py +220 -0
  32. tests/regscale/integrations/public/test_cci.py +458 -0
  33. tests/regscale/integrations/public/test_cisa.py +1021 -0
  34. tests/regscale/integrations/public/test_emass.py +518 -0
  35. tests/regscale/integrations/public/test_fedramp.py +851 -0
  36. tests/regscale/integrations/public/test_fedramp_cis_crm.py +3661 -0
  37. tests/regscale/integrations/public/test_file_uploads.py +506 -0
  38. tests/regscale/integrations/public/test_oscal.py +453 -0
  39. tests/regscale/models/test_form_field_value_integration.py +304 -0
  40. tests/regscale/models/test_module_integration.py +582 -0
  41. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/LICENSE +0 -0
  42. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/WHEEL +0 -0
  43. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/entry_points.txt +0 -0
  44. {regscale_cli-6.23.0.0.dist-info → regscale_cli-6.24.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Test OSCAL Integrations"""
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from typing import Tuple
10
+
11
+ import pytest
12
+
13
+ from regscale.core.app.utils.app_utils import check_file_path
14
+ from regscale.integrations.public.oscal import (
15
+ process_component,
16
+ process_fedramp_objectives,
17
+ upload_catalog,
18
+ upload_profile,
19
+ )
20
+ from regscale.utils.threading.threadhandler import create_threads, thread_assignment
21
+ from tests import CLITestFixture
22
+
23
+ sys.path.append("..") # Adds higher directory to python modules path.
24
+
25
+
26
+ class TestOscal(CLITestFixture):
27
+ """Oscal Test Class"""
28
+
29
+ @pytest.fixture(autouse=True)
30
+ def oscal_catalog(self):
31
+ """Test OSCAL Catalog"""
32
+ catalog_path = self.get_tests_dir("tests/test_data/NIST-800-53r4_catalog_MIN.json")
33
+ with open(catalog_path.absolute(), "r", encoding="utf-8") as infile:
34
+ data = json.load(infile)
35
+ return data
36
+
37
+ @pytest.fixture(autouse=True)
38
+ def oscal_control(self, oscal_catalog):
39
+ """Test OSCAL Control"""
40
+ control = oscal_catalog["catalog"]["groups"][0]["controls"][0]
41
+ return control
42
+
43
+ @staticmethod
44
+ @pytest.fixture(autouse=True)
45
+ def sample_control():
46
+ """
47
+ Provides a sample security control for tests.
48
+
49
+ :return: A sample security control
50
+ :rtype: dict
51
+ """
52
+ return {"id": "ctrl-1"}
53
+
54
+ @staticmethod
55
+ @pytest.fixture(autouse=True)
56
+ def sample_part_with_prose():
57
+ """
58
+ Provides a sample part with prose for tests.
59
+
60
+ :return: A sample part with prose
61
+ :rtype: dict
62
+ """
63
+ return {"id": "part-1", "prose": "Sample prose.", "name": "statement"}
64
+
65
+ @staticmethod
66
+ @pytest.fixture(autouse=True)
67
+ def sample_part_with_nested_parts():
68
+ """
69
+ Provides a sample part with nested parts for tests.
70
+
71
+ :return: A sample part with nested parts
72
+ :rtype: dict
73
+ """
74
+ return {
75
+ "id": "part-2",
76
+ "name": "objective",
77
+ "parts": [
78
+ {"id": "part-2-1", "name": "item", "props": [{"prose": "nested-prop-value"}], "prose": "Nested prose."}
79
+ ],
80
+ "prose": "First prose.",
81
+ }
82
+
83
+ @staticmethod
84
+ @pytest.fixture(autouse=True)
85
+ def sample_part_deeply_nested():
86
+ """
87
+ Provides a sample part with deeply nested parts for tests.
88
+
89
+ :return: A sample part with deeply nested parts
90
+ :rtype: dict
91
+ """
92
+ return {
93
+ "id": "part-3",
94
+ "name": "objective",
95
+ "prose": "First prose.",
96
+ "parts": [
97
+ {
98
+ "id": "part-3-1",
99
+ "name": "item",
100
+ "prose": "Second prose.",
101
+ "parts": [{"id": "part-3-1-1", "name": "objective", "prose": "Deeply nested prose."}],
102
+ }
103
+ ],
104
+ }
105
+
106
+ @staticmethod
107
+ @pytest.fixture(autouse=True)
108
+ def sample_part_with_super_deep_nested_parts():
109
+ """
110
+ Provides a sample part with super deep nested parts for tests.
111
+
112
+ :return: A sample part with super deep nested parts
113
+ :rtype: dict
114
+ """
115
+ return {
116
+ "id": "part-4",
117
+ "name": "objective",
118
+ "prose": "First prose.",
119
+ "parts": [
120
+ {
121
+ "id": "part-2-1",
122
+ "name": "item",
123
+ "props": [{"value": "nested-prop-value"}],
124
+ "prose": "Second prose.",
125
+ "parts": [
126
+ {
127
+ "id": "part-3-1",
128
+ "name": "objective",
129
+ "prose": "Third prose.",
130
+ "parts": [
131
+ {
132
+ "id": "part-3-1-1",
133
+ "name": "item",
134
+ "prose": "Fourth prose.",
135
+ "parts": [
136
+ {
137
+ "id": "part-3-1-1-1",
138
+ "name": "objective",
139
+ "prose": "Fifth prose.",
140
+ "parts": [
141
+ {
142
+ "id": "part-3-1-1-1-1",
143
+ "name": "item",
144
+ "prose": "Sixth prose.",
145
+ "parts": [
146
+ {
147
+ "id": "part-3-1-1-1-1-1",
148
+ "name": "objective",
149
+ "prose": "Seventh prose.",
150
+ "parts": [
151
+ {
152
+ "id": "part-3-1-1-1-1-1-1",
153
+ "name": "item",
154
+ }
155
+ ],
156
+ },
157
+ ],
158
+ }
159
+ ],
160
+ }
161
+ ],
162
+ }
163
+ ],
164
+ },
165
+ ],
166
+ }
167
+ ],
168
+ }
169
+
170
+ @staticmethod
171
+ def test_no_prose_or_name(sample_control):
172
+ part = {"id": "part-0"}
173
+ objectives = []
174
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
175
+ assert result == ""
176
+ assert len(objectives) == 0
177
+
178
+ @staticmethod
179
+ def test_with_prose_only(sample_part_with_prose, sample_control):
180
+ part = sample_part_with_prose
181
+ objectives = []
182
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
183
+ assert "Sample prose." in result
184
+ assert len(objectives) == 1
185
+
186
+ @staticmethod
187
+ def test_with_name_item(sample_control):
188
+ part = {"id": "part-1", "name": "item", "prose": "Item prose."}
189
+ objectives = []
190
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
191
+ assert "Item prose." in result
192
+ assert len(objectives) == 1
193
+
194
+ @staticmethod
195
+ def test_with_nested_parts(sample_part_with_nested_parts, sample_control):
196
+ part = sample_part_with_nested_parts
197
+ objectives = []
198
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
199
+ assert "First prose." in result
200
+ assert len(objectives) == 1
201
+
202
+ @staticmethod
203
+ def test_with_deeply_nested_parts(sample_part_deeply_nested, sample_control):
204
+ part = sample_part_deeply_nested
205
+ objectives = []
206
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
207
+ assert "First prose." in result
208
+ assert len(objectives) == 1
209
+
210
+ @staticmethod
211
+ def test_with_multiple_nested_parts(
212
+ sample_part_with_nested_parts,
213
+ sample_part_deeply_nested,
214
+ sample_control,
215
+ sample_part_with_super_deep_nested_parts,
216
+ ):
217
+ part = {
218
+ "id": "part-4",
219
+ "name": "objective",
220
+ "parts": [
221
+ sample_part_with_nested_parts,
222
+ sample_part_deeply_nested,
223
+ sample_part_with_super_deep_nested_parts,
224
+ ],
225
+ }
226
+ objectives = []
227
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
228
+ assert len(objectives) == 3
229
+ assert result == ""
230
+
231
+ @staticmethod
232
+ def test_no_parts(sample_control):
233
+ part = {"id": "part-5", "name": "objective", "prose": "No parts prose."}
234
+ objectives = []
235
+ result = process_fedramp_objectives(part, "", objectives, sample_control)
236
+ assert "No parts prose." in result
237
+ assert len(objectives) == 1
238
+
239
+ def test_create_catalog(self, oscal_catalog):
240
+ """Test Catalog Code"""
241
+ check_file_path("processing")
242
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
243
+ # change the catalog name to the name of the test run id
244
+ data = oscal_catalog
245
+ with open(tmp_file.name, "w", encoding="utf-8") as outfile:
246
+ title = data["catalog"]["metadata"]["title"]
247
+ data["catalog"]["metadata"]["title"] = title.replace("(TEST)", self.title_prefix)
248
+ cat_name = data["catalog"]["metadata"]["title"]
249
+ json.dump(data, outfile, indent=4)
250
+ # Pass default argument to click function
251
+ self.logger.debug(tmp_file.name)
252
+
253
+ self.upload_catalog(tmp_file.name)
254
+ # delete extra data after we are finished
255
+ self.delete_catalog_items()
256
+ # delete the catalog
257
+ self.delete_inserted_catalog(cat_name)
258
+ self.logger.debug(cat_name)
259
+ tmp_file.close()
260
+ os.remove(tmp_file.name)
261
+
262
+ def test_create_profile(self):
263
+ """Test Profile Code"""
264
+ if not os.path.exists("processing"):
265
+ os.mkdir("processing")
266
+ # Need a runner to allow click to work with pytest
267
+ test_file_path = self.get_tests_dir("tests") / "test_data/fedramp_high_profile.json"
268
+ with open(test_file_path, "r", encoding="utf-8") as infile:
269
+ data = json.load(infile)
270
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
271
+ with open(tmp_file.name, "w", encoding="utf-8") as outfile:
272
+ json.dump(data, outfile, indent=4)
273
+ self.logger.debug(outfile.name)
274
+ # change the profile title to the name of the test run id
275
+ with open(tmp_file.name, "r", encoding="utf-8") as infile:
276
+ data = json.load(infile)
277
+ with open(tmp_file.name, "w", encoding="utf-8") as outfile:
278
+ title = data["profile"]["metadata"]["title"]
279
+ data["profile"]["metadata"]["title"] = self.title_prefix + title
280
+ prof_name = data["profile"]["metadata"]["title"]
281
+ json.dump(data, outfile, indent=4)
282
+ self.logger.debug(prof_name)
283
+ # Pass default argument to click function
284
+
285
+ self.upload_profile(file_name=test_file_path, title=prof_name)
286
+ # delete extra data after we are finished
287
+ self.delete_inserted_profile(prof_name)
288
+ self.logger.debug(prof_name)
289
+ tmp_file.close()
290
+ os.remove(tmp_file.name)
291
+
292
+ def test_create_component(self):
293
+ """Test Component Code"""
294
+ if not os.path.exists("processing"):
295
+ os.mkdir("processing")
296
+ component_file_path = self.get_tests_dir("tests") / "test_data/oscal_component.yaml"
297
+ with open(component_file_path, "r", encoding="utf-8") as infile:
298
+ data = infile.read()
299
+ self.logger.debug(data)
300
+ assert data
301
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
302
+ tmp_file.write(bytes(data, "utf-8"))
303
+ tmp_file.close()
304
+ os.rename(tmp_file.name, tmp_file.name + ".yaml")
305
+ filename = tmp_file.name + ".yaml"
306
+ process_component(filename)
307
+ os.remove(filename)
308
+
309
+ @staticmethod
310
+ def upload_profile(file_name, title) -> None:
311
+ """
312
+ Upload the catalog
313
+
314
+ :param str file_name: file path to the catalog to upload to RegScale
315
+ :param str title: title of the catalog
316
+ :rtype: None
317
+ """
318
+ from pathlib import Path
319
+
320
+ upload_profile(title=title, catalog=84, categorization="Moderate", file_name=Path(file_name))
321
+
322
+ @staticmethod
323
+ def upload_catalog(file_name) -> None:
324
+ """Upload the catalog"""
325
+ upload_catalog(file_name=file_name)
326
+
327
+ def delete_catalog_items(self):
328
+ """testing out deleting items for a catalog for debugging"""
329
+ # update api pool limits to max_thread count from init.yaml
330
+ self.api.pool_connections = (
331
+ self.config["maxThreads"]
332
+ if self.config["maxThreads"] > self.api.pool_connections
333
+ else self.api.pool_connections
334
+ )
335
+ self.api.pool_maxsize = (
336
+ self.config["maxThreads"] if self.config["maxThreads"] > self.api.pool_maxsize else self.api.pool_maxsize
337
+ )
338
+ inserted_items: list[dict] = [
339
+ {"file_name": "newParameters.json", "regscale_module": "controlParameters"},
340
+ {
341
+ "file_name": "newTests.json",
342
+ "regscale_module": "controlTestPlans",
343
+ },
344
+ {
345
+ "file_name": "newObjectives.json",
346
+ "regscale_module": "controlObjectives",
347
+ },
348
+ {
349
+ "file_name": "newControls.json",
350
+ "regscale_module": "securitycontrols",
351
+ },
352
+ ]
353
+ for file in inserted_items:
354
+ with open(f".{os.sep}processing{os.sep}{file['file_name']}", "r", encoding="utf-8") as infile:
355
+ data = json.load(infile)
356
+ create_threads(
357
+ process=self.delete_inserted_items,
358
+ args=(
359
+ data,
360
+ file["regscale_module"],
361
+ self.app.config,
362
+ self.api,
363
+ self.logger,
364
+ ),
365
+ thread_count=len(data),
366
+ )
367
+
368
+ def delete_inserted_catalog(self, cat_name):
369
+ """delete catalog"""
370
+ from regscale.models.regscale_models import Catalog
371
+
372
+ cat_found = False
373
+ cats = Catalog.get_list()
374
+ for cat in cats:
375
+ if cat.title == cat_name:
376
+ delete_this_cat = cat
377
+ cat_found = True
378
+ break
379
+ assert cat_found is True
380
+ self.logger.info(delete_this_cat.model_dump()) # noqa
381
+ response = delete_this_cat.delete()
382
+ assert response is True
383
+
384
+ def delete_inserted_profile(self, prof_name):
385
+ """delete profile"""
386
+ from regscale.models.regscale_models import Profile
387
+
388
+ profs = Profile.get_list()
389
+ delete_this_prof = [prof for prof in profs if prof.name == prof_name][0]
390
+ self.logger.info(delete_this_prof.model_dump())
391
+ assert delete_this_prof is not None
392
+ response = delete_this_prof.delete()
393
+ assert response is True
394
+
395
+ @staticmethod
396
+ def delete_inserted_items(args: Tuple, thread: int) -> None:
397
+ """
398
+ Delete items that were added to the catalog
399
+ :rtype: None
400
+ """
401
+ inserted_items, regscale_module, config, api, logger = args
402
+ headers = {
403
+ "accept": "*/*",
404
+ "Authorization": config["token"],
405
+ }
406
+ # find which records should be executed by the current thread
407
+ threads = thread_assignment(thread=thread, total_items=len(inserted_items))
408
+
409
+ # iterate through the thread assignment items and process them
410
+ for i in range(len(threads)):
411
+ # set the recommendation for the thread for later use in the function
412
+ item = inserted_items[threads[i]]
413
+
414
+ url = f'{config["domain"]}/api/{regscale_module}/{item["id"]}'
415
+ response = api.delete(url=url, headers=headers)
416
+ if response.status_code == 200:
417
+ logger.info("Deleted #%s from %s\n%s", item["id"], regscale_module, item)
418
+ else:
419
+ logger.error(
420
+ "Unable to delete #%s from %s\n%s",
421
+ item["id"],
422
+ regscale_module,
423
+ item,
424
+ )
425
+
426
+ @staticmethod
427
+ def test_empty_part():
428
+ part = {}
429
+ str_obj = ""
430
+ objectives = []
431
+ ctrl = {}
432
+ result = process_fedramp_objectives(part, str_obj, objectives, ctrl)
433
+ assert result == ""
434
+
435
+ @staticmethod
436
+ def test_create_new_objective():
437
+ part = {"name": "item", "prose": "This is a test objective", "id": "test_id"}
438
+ str_obj = ""
439
+ objectives = []
440
+ ctrl = {"id": "test_ctrl_id"}
441
+ _ = process_fedramp_objectives(part, str_obj, objectives, ctrl)
442
+ assert len(objectives) == 1
443
+ assert objectives[0]["name"] == "test_id"
444
+ assert objectives[0]["part_id"] == "test_ctrl_id"
445
+
446
+ @staticmethod
447
+ def test_nested_control_parts(oscal_control):
448
+ part = oscal_control["parts"][0]
449
+ str_obj = ""
450
+ objectives = []
451
+ result = process_fedramp_objectives(part, str_obj, objectives, oscal_control)
452
+ assert result == "The organization: "
453
+ assert len(objectives) == 4